mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-16 04:20:30 +00:00
(WIP) Rewrite parts of the app in Swift and fix iOS 26.4 issues (#358)
* Rewrite parts of the app in Swift and fix iOS 26.4 issues (WIP) * Add more PID stuff * Improve location simulation * whoops
This commit is contained in:
parent
789502aeeb
commit
5db5f49117
43 changed files with 3559 additions and 2582 deletions
|
|
@ -448,14 +448,15 @@
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = XXK735HX49;
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
||||||
"$(inherited)",
|
|
||||||
"$(PROJECT_DIR)/StikJIT/Sources",
|
|
||||||
);
|
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
HEADER_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"$(PROJECT_DIR)/StikJIT/idevice",
|
||||||
|
);
|
||||||
INFOPLIST_FILE = StikJIT/Info.plist;
|
INFOPLIST_FILE = StikJIT/Info.plist;
|
||||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||||
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
|
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
|
||||||
|
|
@ -490,7 +491,7 @@
|
||||||
SUPPORTS_MACCATALYST = NO;
|
SUPPORTS_MACCATALYST = NO;
|
||||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "StikJIT/StikJIT-Bridging-Header.h";
|
SWIFT_INCLUDE_PATHS = "$(inherited) $(PROJECT_DIR)/StikJIT/idevice";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
|
@ -509,14 +510,15 @@
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = XXK735HX49;
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
||||||
"$(inherited)",
|
|
||||||
"$(PROJECT_DIR)/StikJIT/Sources",
|
|
||||||
);
|
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
HEADER_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"$(PROJECT_DIR)/StikJIT/idevice",
|
||||||
|
);
|
||||||
INFOPLIST_FILE = StikJIT/Info.plist;
|
INFOPLIST_FILE = StikJIT/Info.plist;
|
||||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||||
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
|
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
|
||||||
|
|
@ -551,7 +553,7 @@
|
||||||
SUPPORTS_MACCATALYST = NO;
|
SUPPORTS_MACCATALYST = NO;
|
||||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "StikJIT/StikJIT-Bridging-Header.h";
|
SWIFT_INCLUDE_PATHS = "$(inherited) $(PROJECT_DIR)/StikJIT/idevice";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
XROS_DEPLOYMENT_TARGET = 2.2;
|
XROS_DEPLOYMENT_TARGET = 2.2;
|
||||||
|
|
|
||||||
|
|
@ -1,126 +0,0 @@
|
||||||
//
|
|
||||||
// IDeviceJSBridgeDebugProxy.m
|
|
||||||
// StikJIT
|
|
||||||
//
|
|
||||||
// Created by s s on 2025/4/25.
|
|
||||||
//
|
|
||||||
@import Foundation;
|
|
||||||
@import JavaScriptCore;
|
|
||||||
#import "JSSupport.h"
|
|
||||||
#import "../idevice/JITEnableContext.h"
|
|
||||||
#import "../idevice/idevice.h"
|
|
||||||
#include "../idevice/jit.h"
|
|
||||||
|
|
||||||
NSString* handleJSContextSendDebugCommand(JSContext* context, NSString* commandStr, DebugProxyHandle* debugProxy) {
|
|
||||||
DebugserverCommandHandle* command = 0;
|
|
||||||
|
|
||||||
command = debugserver_command_new([commandStr UTF8String], NULL, 0);
|
|
||||||
|
|
||||||
char* attach_response = 0;
|
|
||||||
IdeviceFfiError* err = debug_proxy_send_command(debugProxy, command, &attach_response);
|
|
||||||
debugserver_command_free(command);
|
|
||||||
if (err) {
|
|
||||||
context.exception = [JSValue valueWithObject:[NSString stringWithFormat:@"error code %d, msg %s", err->code, err->message] inContext:context];
|
|
||||||
idevice_error_free(err);
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
NSString* commandResponse = nil;
|
|
||||||
if(attach_response) {
|
|
||||||
commandResponse = @(attach_response);
|
|
||||||
}
|
|
||||||
idevice_string_free(attach_response);
|
|
||||||
return commandResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 0 <= val <= 15
|
|
||||||
char u8toHexChar(uint8_t val) {
|
|
||||||
if(val < 10) {
|
|
||||||
return val + '0';
|
|
||||||
} else {
|
|
||||||
return val + 87;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void calcAndWriteCheckSum(char* commandStart) {
|
|
||||||
uint8_t sum = 0;
|
|
||||||
char* cur = commandStart;
|
|
||||||
for(; *cur != '#'; ++cur) {
|
|
||||||
sum += *cur;
|
|
||||||
}
|
|
||||||
cur[1] = u8toHexChar((sum & 0xf0) >> 4);
|
|
||||||
cur[2] = u8toHexChar(sum & 0xf);
|
|
||||||
}
|
|
||||||
|
|
||||||
// support up to 9 digit
|
|
||||||
void writeAddress(char* writeStart, uint64_t addr) {
|
|
||||||
writeStart[0] = u8toHexChar((addr & 0xf00000000) >> 32);
|
|
||||||
writeStart[1] = u8toHexChar((addr & 0xf0000000) >> 28);
|
|
||||||
writeStart[2] = u8toHexChar((addr & 0xf000000) >> 24);
|
|
||||||
writeStart[3] = u8toHexChar((addr & 0xf00000) >> 20);
|
|
||||||
writeStart[4] = u8toHexChar((addr & 0xf0000) >> 16);
|
|
||||||
writeStart[5] = u8toHexChar((addr & 0xf000) >> 12);
|
|
||||||
writeStart[6] = u8toHexChar((addr & 0xf00) >> 8);
|
|
||||||
writeStart[7] = u8toHexChar((addr & 0xf0) >> 4);
|
|
||||||
writeStart[8] = u8toHexChar((addr & 0xf));
|
|
||||||
}
|
|
||||||
|
|
||||||
// you need to free generated buffer
|
|
||||||
char* getBulkMemWriteCommand(uint64_t startAddr, uint64_t JITPagesSize, uint32_t* commandCountOut, uint32_t* bufferLengthOut) {
|
|
||||||
// $M10c128000,1:69#12
|
|
||||||
uint32_t commandCount = (uint32_t)(JITPagesSize >> 14);
|
|
||||||
uint32_t commandBufferSize = commandCount * 19;
|
|
||||||
*commandCountOut = commandCount;
|
|
||||||
*bufferLengthOut = commandBufferSize;
|
|
||||||
char* buffer = malloc(commandBufferSize + 1);
|
|
||||||
char* bufferEnd = buffer + commandBufferSize;
|
|
||||||
buffer[commandBufferSize] = 0;
|
|
||||||
|
|
||||||
uint64_t curAddr = startAddr;
|
|
||||||
for(char* curBufferPtr = buffer; curBufferPtr < bufferEnd; curBufferPtr += 19) {
|
|
||||||
curBufferPtr[0] = '$';
|
|
||||||
curBufferPtr[1] = 'M';
|
|
||||||
curBufferPtr[11] = ',';
|
|
||||||
curBufferPtr[12] = '1';
|
|
||||||
curBufferPtr[13] = ':';
|
|
||||||
curBufferPtr[14] = '6';
|
|
||||||
curBufferPtr[15] = '9';
|
|
||||||
curBufferPtr[16] = '#';
|
|
||||||
writeAddress(curBufferPtr + 2, curAddr);
|
|
||||||
calcAndWriteCheckSum(curBufferPtr + 1);
|
|
||||||
curAddr += 16384;
|
|
||||||
}
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSString* handleJITPageWrite(JSContext* context, uint64_t startAddr, uint64_t JITPagesSize, DebugProxyHandle* debugProxy) {
|
|
||||||
uint32_t bufferLength = 0;
|
|
||||||
uint32_t commandCount = 0;
|
|
||||||
char* commandBuffer = getBulkMemWriteCommand(startAddr, JITPagesSize, &commandCount, &bufferLength);
|
|
||||||
// we send 1024 commands at a time
|
|
||||||
for(int curCommand = 0; curCommand < commandCount; curCommand += 1024) {
|
|
||||||
uint32_t commandsToSend = (commandCount - curCommand > 1024) ? 1024 : (commandCount - curCommand);
|
|
||||||
IdeviceFfiError* err = debug_proxy_send_raw(debugProxy, (const uint8_t *)commandBuffer + curCommand * 19, commandsToSend * 19);
|
|
||||||
if(err) {
|
|
||||||
context.exception = [JSValue valueWithObject:[NSString stringWithFormat:@"error code %d, msg %s", err->code, err->message] inContext:context];
|
|
||||||
free(commandBuffer);
|
|
||||||
idevice_error_free(err);
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
for(int i = 0; i < commandsToSend; ++i) {
|
|
||||||
char* response = 0;
|
|
||||||
IdeviceFfiError* err = debug_proxy_read_response(debugProxy, &response);
|
|
||||||
if(response) {
|
|
||||||
idevice_string_free(response);
|
|
||||||
}
|
|
||||||
if(err) {
|
|
||||||
context.exception = [JSValue valueWithObject:[NSString stringWithFormat:@"error code %d, msg %s", err->code, err->message] inContext:context];
|
|
||||||
free(commandBuffer);
|
|
||||||
idevice_error_free(err);
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
free(commandBuffer);
|
|
||||||
return @"OK";
|
|
||||||
}
|
|
||||||
161
StikJIT/JSSupport/JSDebugSupport.swift
Normal file
161
StikJIT/JSSupport/JSDebugSupport.swift
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
//
|
||||||
|
// JSDebugSupport.swift
|
||||||
|
// StikDebug
|
||||||
|
//
|
||||||
|
// Created by Stephen on 2026/3/30.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import JavaScriptCore
|
||||||
|
import idevice
|
||||||
|
|
||||||
|
private func jsException(_ message: String, in context: JSContext?) {
|
||||||
|
guard let context else { return }
|
||||||
|
context.exception = JSValue(object: message, in: context)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func describeIdeviceError(_ ffiError: UnsafeMutablePointer<IdeviceFfiError>) -> String {
|
||||||
|
if let message = ffiError.pointee.message {
|
||||||
|
return "error code \(ffiError.pointee.code), msg \(String(cString: message))"
|
||||||
|
}
|
||||||
|
return "error code \(ffiError.pointee.code)"
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleJSContextSendDebugCommand(_ context: JSContext?, _ commandStr: String, _ debugProxy: OpaquePointer?) -> String? {
|
||||||
|
guard let debugProxy else {
|
||||||
|
jsException("debug proxy is unavailable", in: context)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let command = debugserver_command_new(commandStr, nil, 0) else {
|
||||||
|
jsException("failed to allocate debugserver command", in: context)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var response: UnsafeMutablePointer<CChar>?
|
||||||
|
let ffiError = debug_proxy_send_command(debugProxy, command, &response)
|
||||||
|
debugserver_command_free(command)
|
||||||
|
|
||||||
|
if let ffiError {
|
||||||
|
jsException(describeIdeviceError(ffiError), in: context)
|
||||||
|
idevice_error_free(ffiError)
|
||||||
|
if let response {
|
||||||
|
idevice_string_free(response)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
defer {
|
||||||
|
if let response {
|
||||||
|
idevice_string_free(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let response else { return nil }
|
||||||
|
return String(cString: response)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func hexCharacter(for value: UInt8) -> UInt8 {
|
||||||
|
if value < 10 {
|
||||||
|
return value + Character("0").asciiValue!
|
||||||
|
}
|
||||||
|
return value + 87
|
||||||
|
}
|
||||||
|
|
||||||
|
private func fillAddress(into buffer: inout [UInt8], at index: Int, address: UInt64) {
|
||||||
|
let masks: [UInt64] = [
|
||||||
|
0xf00000000,
|
||||||
|
0x0f0000000,
|
||||||
|
0x00f000000,
|
||||||
|
0x000f00000,
|
||||||
|
0x0000f0000,
|
||||||
|
0x00000f000,
|
||||||
|
0x000000f00,
|
||||||
|
0x0000000f0,
|
||||||
|
0x00000000f,
|
||||||
|
]
|
||||||
|
|
||||||
|
for (offset, mask) in masks.enumerated() {
|
||||||
|
let shift = UInt64((masks.count - 1 - offset) * 4)
|
||||||
|
let nibble = UInt8((address & mask) >> shift)
|
||||||
|
buffer[index + offset] = hexCharacter(for: nibble)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func writeChecksum(into buffer: inout [UInt8], at startIndex: Int) {
|
||||||
|
var checksum: UInt8 = 0
|
||||||
|
var index = startIndex
|
||||||
|
while buffer[index] != Character("#").asciiValue! {
|
||||||
|
checksum &+= buffer[index]
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer[index + 1] = hexCharacter(for: (checksum & 0xf0) >> 4)
|
||||||
|
buffer[index + 2] = hexCharacter(for: checksum & 0x0f)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeBulkWriteCommands(startAddress: UInt64, pageSize: UInt64) -> [UInt8] {
|
||||||
|
let commandCount = Int(pageSize >> 14)
|
||||||
|
var buffer = [UInt8](repeating: 0, count: commandCount * 19)
|
||||||
|
|
||||||
|
var currentAddress = startAddress
|
||||||
|
for commandIndex in 0..<commandCount {
|
||||||
|
let start = commandIndex * 19
|
||||||
|
buffer[start + 0] = Character("$").asciiValue!
|
||||||
|
buffer[start + 1] = Character("M").asciiValue!
|
||||||
|
fillAddress(into: &buffer, at: start + 2, address: currentAddress)
|
||||||
|
buffer[start + 11] = Character(",").asciiValue!
|
||||||
|
buffer[start + 12] = Character("1").asciiValue!
|
||||||
|
buffer[start + 13] = Character(":").asciiValue!
|
||||||
|
buffer[start + 14] = Character("6").asciiValue!
|
||||||
|
buffer[start + 15] = Character("9").asciiValue!
|
||||||
|
buffer[start + 16] = Character("#").asciiValue!
|
||||||
|
writeChecksum(into: &buffer, at: start + 1)
|
||||||
|
currentAddress += 16_384
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleJITPageWrite(_ context: JSContext?, _ startAddr: UInt64, _ jitPagesSize: UInt64, _ debugProxy: OpaquePointer?) -> String? {
|
||||||
|
guard let debugProxy else {
|
||||||
|
jsException("debug proxy is unavailable", in: context)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let commandBuffer = makeBulkWriteCommands(startAddress: startAddr, pageSize: jitPagesSize)
|
||||||
|
let commandCount = Int(jitPagesSize >> 14)
|
||||||
|
let commandsPerBatch = 128
|
||||||
|
|
||||||
|
for batchStart in stride(from: 0, to: commandCount, by: commandsPerBatch) {
|
||||||
|
let commandsToSend = min(commandsPerBatch, commandCount - batchStart)
|
||||||
|
let byteOffset = batchStart * 19
|
||||||
|
let byteCount = commandsToSend * 19
|
||||||
|
|
||||||
|
let ffiError = commandBuffer.withUnsafeBytes { rawBuffer -> UnsafeMutablePointer<IdeviceFfiError>? in
|
||||||
|
let baseAddress = rawBuffer.bindMemory(to: UInt8.self).baseAddress!
|
||||||
|
return debug_proxy_send_raw(debugProxy, baseAddress.advanced(by: byteOffset), UInt(byteCount))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let ffiError {
|
||||||
|
jsException(describeIdeviceError(ffiError), in: context)
|
||||||
|
idevice_error_free(ffiError)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _ in 0..<commandsToSend {
|
||||||
|
var response: UnsafeMutablePointer<CChar>?
|
||||||
|
let ffiError = debug_proxy_read_response(debugProxy, &response)
|
||||||
|
if let response {
|
||||||
|
idevice_string_free(response)
|
||||||
|
}
|
||||||
|
if let ffiError {
|
||||||
|
jsException(describeIdeviceError(ffiError), in: context)
|
||||||
|
idevice_error_free(ffiError)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
//
|
|
||||||
// JSSupport.h
|
|
||||||
// StikJIT
|
|
||||||
//
|
|
||||||
// Created by s s on 2025/4/24.
|
|
||||||
//
|
|
||||||
@import WebKit;
|
|
||||||
@import JavaScriptCore;
|
|
||||||
#include "../idevice/jit.h"
|
|
||||||
|
|
||||||
NSString* handleJSContextSendDebugCommand(JSContext* context, NSString* commandStr, DebugProxyHandle* debugProxy);
|
|
||||||
NSString* handleJITPageWrite(JSContext* context, uint64_t startAddr, uint64_t JITPagesSize, DebugProxyHandle* debugProxy);
|
|
||||||
|
|
@ -7,11 +7,12 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import JavaScriptCore
|
import JavaScriptCore
|
||||||
|
import idevice
|
||||||
|
|
||||||
typealias RemoteServerHandle = OpaquePointer
|
typealias RemoteServerHandle = OpaquePointer
|
||||||
typealias ScreenshotClientHandle = OpaquePointer
|
typealias ScreenshotClientHandle = OpaquePointer
|
||||||
|
|
||||||
class RunJSViewModel: ObservableObject {
|
final class RunJSViewModel: ObservableObject, @unchecked Sendable {
|
||||||
var context: JSContext?
|
var context: JSContext?
|
||||||
@Published var logs: [String] = []
|
@Published var logs: [String] = []
|
||||||
@Published var scriptName: String = "Script"
|
@Published var scriptName: String = "Script"
|
||||||
|
|
@ -19,9 +20,9 @@ class RunJSViewModel: ObservableObject {
|
||||||
var pid: Int
|
var pid: Int
|
||||||
var debugProxy: OpaquePointer?
|
var debugProxy: OpaquePointer?
|
||||||
var remoteServer: OpaquePointer?
|
var remoteServer: OpaquePointer?
|
||||||
var semaphore: dispatch_semaphore_t?
|
var semaphore: DispatchSemaphore?
|
||||||
|
|
||||||
init(pid: Int, debugProxy: OpaquePointer?, remoteServer: OpaquePointer?, semaphore: dispatch_semaphore_t?) {
|
init(pid: Int, debugProxy: OpaquePointer?, remoteServer: OpaquePointer?, semaphore: DispatchSemaphore?) {
|
||||||
self.pid = pid
|
self.pid = pid
|
||||||
self.debugProxy = debugProxy
|
self.debugProxy = debugProxy
|
||||||
self.remoteServer = remoteServer
|
self.remoteServer = remoteServer
|
||||||
|
|
@ -166,7 +167,7 @@ class RunJSViewModel: ObservableObject {
|
||||||
|
|
||||||
private func sanitizedScreenshotName(from preferredName: String?) -> String {
|
private func sanitizedScreenshotName(from preferredName: String?) -> String {
|
||||||
let defaultName = "screenshot-\(Int(Date().timeIntervalSince1970))"
|
let defaultName = "screenshot-\(Int(Date().timeIntervalSince1970))"
|
||||||
guard var candidate = preferredName?.trimmingCharacters(in: .whitespacesAndNewlines),
|
guard let candidate = preferredName?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||||
!candidate.isEmpty else {
|
!candidate.isEmpty else {
|
||||||
return "\(defaultName).png"
|
return "\(defaultName).png"
|
||||||
}
|
}
|
||||||
|
|
@ -215,7 +216,7 @@ struct RunJSView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Running \(model.scriptName)")
|
.navigationTitle("Running \(model.scriptName)")
|
||||||
.onChange(of: model.logs.count) { newCount in
|
.onChange(of: model.logs.count) { _, newCount in
|
||||||
guard newCount > 0 else { return }
|
guard newCount > 0 else { return }
|
||||||
withAnimation {
|
withAnimation {
|
||||||
proxy.scrollTo(newCount - 1, anchor: .bottom)
|
proxy.scrollTo(newCount - 1, anchor: .bottom)
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ struct ScriptListView: View {
|
||||||
@State private var showNewFileAlert = false
|
@State private var showNewFileAlert = false
|
||||||
@State private var newFileName = ""
|
@State private var newFileName = ""
|
||||||
@State private var showImporter = false
|
@State private var showImporter = false
|
||||||
@AppStorage("DefaultScriptName") private var defaultScriptName = "attachDetach.js"
|
@AppStorage(UserDefaults.Keys.defaultScriptName) private var defaultScriptName = UserDefaults.Keys.defaultScriptNameValue
|
||||||
|
|
||||||
@State private var isBusy = false
|
@State private var isBusy = false
|
||||||
@State private var alertVisible = false
|
@State private var alertVisible = false
|
||||||
|
|
@ -200,43 +200,14 @@ struct ScriptListView: View {
|
||||||
|
|
||||||
// MARK: - File Ops
|
// MARK: - File Ops
|
||||||
|
|
||||||
private func scriptsDirectory() -> URL {
|
private func scriptsDirectory() throws -> URL {
|
||||||
let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
let directory = try ScriptStore.prepareDirectory()
|
||||||
.appendingPathComponent("scripts")
|
try ensureEditorScripts(in: directory)
|
||||||
var isDir: ObjCBool = false
|
return directory
|
||||||
let exists = FileManager.default.fileExists(atPath: dir.path, isDirectory: &isDir)
|
|
||||||
do {
|
|
||||||
if exists && !isDir.boolValue {
|
|
||||||
try FileManager.default.removeItem(at: dir)
|
|
||||||
}
|
|
||||||
if !exists || !isDir.boolValue {
|
|
||||||
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
|
||||||
}
|
|
||||||
try ensureDefaultScripts(in: dir)
|
|
||||||
} catch {
|
|
||||||
presentError(title: "Unable to Create Scripts Folder", message: error.localizedDescription)
|
|
||||||
}
|
|
||||||
return dir
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func ensureDefaultScripts(in directory: URL) throws {
|
private func ensureEditorScripts(in directory: URL) throws {
|
||||||
let fm = FileManager.default
|
let fm = FileManager.default
|
||||||
let bundledScripts: [(resource: String, filename: String)] = [
|
|
||||||
("attachDetach", "attachDetach.js"),
|
|
||||||
("maciOS", "maciOS.js"),
|
|
||||||
("Amethyst-MeloNX", "Amethyst-MeloNX.js"),
|
|
||||||
("Geode", "Geode.js"),
|
|
||||||
("manic", "manic.js"),
|
|
||||||
("UTM-Dolphin", "UTM-Dolphin.js")
|
|
||||||
]
|
|
||||||
for entry in bundledScripts {
|
|
||||||
if let bundleURL = Bundle.main.url(forResource: entry.resource, withExtension: "js") {
|
|
||||||
let destination = directory.appendingPathComponent(entry.filename)
|
|
||||||
if !fm.fileExists(atPath: destination.path) {
|
|
||||||
try fm.copyItem(at: bundleURL, to: destination)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let screenshotURL = directory.appendingPathComponent("screenshot-demo.js")
|
let screenshotURL = directory.appendingPathComponent("screenshot-demo.js")
|
||||||
if !fm.fileExists(atPath: screenshotURL.path) {
|
if !fm.fileExists(atPath: screenshotURL.path) {
|
||||||
try screenshotDemoScript.write(to: screenshotURL, atomically: true, encoding: .utf8)
|
try screenshotDemoScript.write(to: screenshotURL, atomically: true, encoding: .utf8)
|
||||||
|
|
@ -248,10 +219,15 @@ struct ScriptListView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadScripts() {
|
private func loadScripts() {
|
||||||
let dir = scriptsDirectory()
|
do {
|
||||||
scripts = (try? FileManager.default.contentsOfDirectory(at: dir, includingPropertiesForKeys: nil))?
|
let directory = try scriptsDirectory()
|
||||||
.filter { $0.pathExtension.lowercased() == "js" }
|
scripts = try FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil)
|
||||||
.sorted { $0.lastPathComponent.localizedCaseInsensitiveCompare($1.lastPathComponent) == .orderedAscending } ?? []
|
.filter { $0.pathExtension.lowercased() == "js" }
|
||||||
|
.sorted { $0.lastPathComponent.localizedCaseInsensitiveCompare($1.lastPathComponent) == .orderedAscending }
|
||||||
|
} catch {
|
||||||
|
scripts = []
|
||||||
|
presentError(title: "Unable to Load Scripts", message: error.localizedDescription)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func saveDefaultScript(_ url: URL) {
|
private func saveDefaultScript(_ url: URL) {
|
||||||
|
|
@ -263,12 +239,12 @@ struct ScriptListView: View {
|
||||||
guard !newFileName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
|
guard !newFileName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
|
||||||
var filename = newFileName.trimmingCharacters(in: .whitespacesAndNewlines)
|
var filename = newFileName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
if !filename.hasSuffix(".js") { filename += ".js" }
|
if !filename.hasSuffix(".js") { filename += ".js" }
|
||||||
let newURL = scriptsDirectory().appendingPathComponent(filename)
|
|
||||||
guard !FileManager.default.fileExists(atPath: newURL.path) else {
|
|
||||||
presentError(title: "Failed to Create New Script", message: "A script with the same name already exists.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
do {
|
do {
|
||||||
|
let newURL = try scriptsDirectory().appendingPathComponent(filename)
|
||||||
|
guard !FileManager.default.fileExists(atPath: newURL.path) else {
|
||||||
|
presentError(title: "Failed to Create New Script", message: "A script with the same name already exists.")
|
||||||
|
return
|
||||||
|
}
|
||||||
try "".write(to: newURL, atomically: true, encoding: .utf8)
|
try "".write(to: newURL, atomically: true, encoding: .utf8)
|
||||||
newFileName = ""
|
newFileName = ""
|
||||||
loadScripts()
|
loadScripts()
|
||||||
|
|
@ -282,7 +258,7 @@ struct ScriptListView: View {
|
||||||
do {
|
do {
|
||||||
try FileManager.default.removeItem(at: url)
|
try FileManager.default.removeItem(at: url)
|
||||||
if url.lastPathComponent == defaultScriptName {
|
if url.lastPathComponent == defaultScriptName {
|
||||||
UserDefaults.standard.removeObject(forKey: "DefaultScriptName")
|
UserDefaults.standard.removeObject(forKey: UserDefaults.Keys.defaultScriptName)
|
||||||
}
|
}
|
||||||
loadScripts()
|
loadScripts()
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -295,7 +271,8 @@ struct ScriptListView: View {
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
defer { DispatchQueue.main.async { self.isBusy = false } }
|
defer { DispatchQueue.main.async { self.isBusy = false } }
|
||||||
do {
|
do {
|
||||||
let dest = self.scriptsDirectory().appendingPathComponent(fileURL.lastPathComponent)
|
let directory = try self.scriptsDirectory()
|
||||||
|
let dest = directory.appendingPathComponent(fileURL.lastPathComponent)
|
||||||
if FileManager.default.fileExists(atPath: dest.path) {
|
if FileManager.default.fileExists(atPath: dest.path) {
|
||||||
try FileManager.default.removeItem(at: dest)
|
try FileManager.default.removeItem(at: dest)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
//
|
|
||||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
|
||||||
//
|
|
||||||
|
|
||||||
#include "idevice/JITEnableContext.h"
|
|
||||||
#import "Utilities/ProcessInspectorBridge.h"
|
|
||||||
#include "idevice/idevice.h"
|
|
||||||
#include "JSSupport/JSSupport.h"
|
|
||||||
#include "idevice/ideviceinfo.h"
|
|
||||||
#include "idevice/location_simulation.h"
|
|
||||||
#include "idevice/profiles.h"
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Network
|
import Network
|
||||||
import UniformTypeIdentifiers
|
import idevice
|
||||||
|
|
||||||
// Register default settings before the app starts
|
// Register default settings before the app starts
|
||||||
private func registerAdvancedOptionsDefault() {
|
private func registerAdvancedOptionsDefault() {
|
||||||
|
|
@ -134,7 +134,8 @@ struct HeartbeatApp: App {
|
||||||
if UserDefaults.standard.bool(forKey: "keepAliveAudio") {
|
if UserDefaults.standard.bool(forKey: "keepAliveAudio") {
|
||||||
BackgroundAudioManager.shared.start()
|
BackgroundAudioManager.shared.start()
|
||||||
}
|
}
|
||||||
if let fixMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, #selector(UIDocumentPickerViewController.fix_init(forOpeningContentTypes:asCopy:))),
|
let fixSelector = NSSelectorFromString("fix_initForOpeningContentTypes:asCopy:")
|
||||||
|
if let fixMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, fixSelector),
|
||||||
let origMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, #selector(UIDocumentPickerViewController.init(forOpeningContentTypes:asCopy:))) {
|
let origMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, #selector(UIDocumentPickerViewController.init(forOpeningContentTypes:asCopy:))) {
|
||||||
method_exchangeImplementations(origMethod, fixMethod)
|
method_exchangeImplementations(origMethod, fixMethod)
|
||||||
}
|
}
|
||||||
|
|
@ -177,7 +178,7 @@ struct HeartbeatApp: App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: scenePhase) { newPhase in
|
.onChange(of: scenePhase) { _, newPhase in
|
||||||
handleScenePhaseChange(newPhase)
|
handleScenePhaseChange(newPhase)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -269,7 +270,7 @@ class MountingProgress: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
func isPairing() -> Bool {
|
func isPairing() -> Bool {
|
||||||
let pairingpath = URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path
|
let pairingpath = PairingFileStore.prepareURL().path
|
||||||
var pairingFile: RpPairingFileHandle?
|
var pairingFile: RpPairingFileHandle?
|
||||||
let err = rp_pairing_file_read(pairingpath, &pairingFile)
|
let err = rp_pairing_file_read(pairingpath, &pairingFile)
|
||||||
if err != nil { return false }
|
if err != nil { return false }
|
||||||
|
|
@ -279,7 +280,7 @@ func isPairing() -> Bool {
|
||||||
|
|
||||||
func startTunnelInBackground(showErrorUI: Bool = true) {
|
func startTunnelInBackground(showErrorUI: Bool = true) {
|
||||||
assert(Thread.isMainThread, "startTunnelInBackground must be called on the main thread")
|
assert(Thread.isMainThread, "startTunnelInBackground must be called on the main thread")
|
||||||
let pairingFileURL = URL.documentsDirectory.appendingPathComponent("pairingFile.plist")
|
let pairingFileURL = PairingFileStore.prepareURL()
|
||||||
|
|
||||||
guard FileManager.default.fileExists(atPath: pairingFileURL.path) else {
|
guard FileManager.default.fileExists(atPath: pairingFileURL.path) else {
|
||||||
tunnelStartPending = false
|
tunnelStartPending = false
|
||||||
|
|
@ -321,7 +322,7 @@ func startTunnelInBackground(showErrorUI: Bool = true) {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if code == -9 {
|
if code == -9 {
|
||||||
do {
|
do {
|
||||||
try FileManager.default.removeItem(at: URL.documentsDirectory.appendingPathComponent("pairingFile.plist"))
|
try PairingFileStore.remove()
|
||||||
LogManager.shared.addInfoLog("Removed invalid pairing file")
|
LogManager.shared.addInfoLog("Removed invalid pairing file")
|
||||||
} catch {
|
} catch {
|
||||||
LogManager.shared.addErrorLog("Failed to remove invalid pairing file: \(error.localizedDescription)")
|
LogManager.shared.addErrorLog("Failed to remove invalid pairing file: \(error.localizedDescription)")
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ final class BackgroundAudioManager {
|
||||||
private var engine = AVAudioEngine()
|
private var engine = AVAudioEngine()
|
||||||
private var player = AVAudioPlayerNode()
|
private var player = AVAudioPlayerNode()
|
||||||
private var isRunning = false
|
private var isRunning = false
|
||||||
|
private var persistentEnabled = false
|
||||||
|
private var activityCount = 0
|
||||||
private var healthCheckTimer: Timer?
|
private var healthCheckTimer: Timer?
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
|
|
@ -29,22 +31,54 @@ final class BackgroundAudioManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() {
|
func start() {
|
||||||
isRunning = true
|
persistentEnabled = true
|
||||||
startEngine()
|
refreshRunningState()
|
||||||
startHealthCheck()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func stop() {
|
func stop() {
|
||||||
isRunning = false
|
persistentEnabled = false
|
||||||
healthCheckTimer?.invalidate()
|
refreshRunningState()
|
||||||
healthCheckTimer = nil
|
}
|
||||||
player.stop()
|
|
||||||
engine.stop()
|
func requestStart() {
|
||||||
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
|
activityCount += 1
|
||||||
|
refreshRunningState()
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestStop() {
|
||||||
|
activityCount = max(activityCount - 1, 0)
|
||||||
|
refreshRunningState()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func refreshRunningState() {
|
||||||
|
let shouldRun = persistentEnabled || (activityCount > 0 && UserDefaults.standard.bool(forKey: "keepAliveAudio"))
|
||||||
|
guard shouldRun != isRunning else {
|
||||||
|
if shouldRun {
|
||||||
|
recoverIfNeeded()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isRunning = shouldRun
|
||||||
|
if shouldRun {
|
||||||
|
startEngine()
|
||||||
|
startHealthCheck()
|
||||||
|
} else {
|
||||||
|
healthCheckTimer?.invalidate()
|
||||||
|
healthCheckTimer = nil
|
||||||
|
player.stop()
|
||||||
|
engine.stop()
|
||||||
|
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func startEngine() {
|
private func startEngine() {
|
||||||
do {
|
do {
|
||||||
|
engine.stop()
|
||||||
|
player.stop()
|
||||||
|
engine = AVAudioEngine()
|
||||||
|
player = AVAudioPlayerNode()
|
||||||
|
|
||||||
let session = AVAudioSession.sharedInstance()
|
let session = AVAudioSession.sharedInstance()
|
||||||
try session.setCategory(.playback, options: .mixWithOthers)
|
try session.setCategory(.playback, options: .mixWithOthers)
|
||||||
try session.setActive(true)
|
try session.setActive(true)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UIKit
|
import UIKit
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
|
import idevice
|
||||||
|
|
||||||
// MARK: - Device Info Manager
|
// MARK: - Device Info Manager
|
||||||
|
|
||||||
|
|
@ -59,9 +60,10 @@ final class DeviceInfoManager: ObservableObject {
|
||||||
private func loadInfo() {
|
private func loadInfo() {
|
||||||
busy = true
|
busy = true
|
||||||
Task.detached {
|
Task.detached {
|
||||||
var cXml : UnsafeMutablePointer<CChar>? = nil;
|
let lockdownHandle = await MainActor.run { self.lockdownHandle }
|
||||||
|
var cXml: UnsafeMutablePointer<CChar>?
|
||||||
do {
|
do {
|
||||||
cXml = try await JITEnableContext.shared.ideviceInfoGetXML(withLockdownClient: self.lockdownHandle?.raw)
|
cXml = try JITEnableContext.shared.ideviceInfoGetXML(withLockdownClient: lockdownHandle?.raw)
|
||||||
} catch {
|
} catch {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.error = ("Fetch Error", "Failed to fetch device info \(error)")
|
self.error = ("Fetch Error", "Failed to fetch device info \(error)")
|
||||||
|
|
@ -71,7 +73,7 @@ final class DeviceInfoManager: ObservableObject {
|
||||||
}
|
}
|
||||||
guard let cXml else { return }
|
guard let cXml else { return }
|
||||||
|
|
||||||
defer { free(UnsafeMutableRawPointer(mutating: cXml)) }
|
defer { plist_mem_free(cXml) }
|
||||||
guard let xml = String(validatingUTF8: cXml) else {
|
guard let xml = String(validatingUTF8: cXml) else {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.error = ("Parse Error", "Invalid XML data")
|
self.error = ("Parse Error", "Invalid XML data")
|
||||||
|
|
@ -139,8 +141,7 @@ struct DeviceInfoView: View {
|
||||||
@State private var showShareSheet = false
|
@State private var showShareSheet = false
|
||||||
@State private var justCopied = false
|
@State private var justCopied = false
|
||||||
|
|
||||||
private let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
private var pairingURL: URL { PairingFileStore.prepareURL() }
|
||||||
private var pairingURL: URL { docs.appendingPathComponent("pairingFile.plist") }
|
|
||||||
private var isPaired: Bool { FileManager.default.fileExists(atPath: pairingURL.path) }
|
private var isPaired: Bool { FileManager.default.fileExists(atPath: pairingURL.path) }
|
||||||
|
|
||||||
@State private var searchText = ""
|
@State private var searchText = ""
|
||||||
|
|
@ -274,7 +275,7 @@ struct DeviceInfoView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.fileImporter(isPresented: $importer, allowedContentTypes: [.propertyList]) { result in
|
.fileImporter(isPresented: $importer, allowedContentTypes: PairingFileStore.supportedContentTypes) { result in
|
||||||
if case .success(let url) = result { importPairing(from: url) }
|
if case .success(let url) = result { importPairing(from: url) }
|
||||||
}
|
}
|
||||||
.fileExporter(
|
.fileExporter(
|
||||||
|
|
@ -288,7 +289,7 @@ struct DeviceInfoView: View {
|
||||||
}
|
}
|
||||||
.onAppear { if isPaired { mgr.initAndLoad() } }
|
.onAppear { if isPaired { mgr.initAndLoad() } }
|
||||||
.onDisappear { mgr.cleanup() }
|
.onDisappear { mgr.cleanup() }
|
||||||
.onChange(of: mgr.error?.message) { _ in
|
.onChange(of: mgr.error?.message) { _, _ in
|
||||||
if let err = mgr.error {
|
if let err = mgr.error {
|
||||||
fail(err.title, err.message)
|
fail(err.title, err.message)
|
||||||
mgr.error = nil
|
mgr.error = nil
|
||||||
|
|
@ -352,14 +353,8 @@ struct DeviceInfoView: View {
|
||||||
// MARK: - Pairing Import
|
// MARK: - Pairing Import
|
||||||
|
|
||||||
private func importPairing(from src: URL) {
|
private func importPairing(from src: URL) {
|
||||||
guard src.startAccessingSecurityScopedResource() else { return }
|
|
||||||
defer { src.stopAccessingSecurityScopedResource() }
|
|
||||||
do {
|
do {
|
||||||
if FileManager.default.fileExists(atPath: pairingURL.path) {
|
try PairingFileStore.importFromPicker(src)
|
||||||
try FileManager.default.removeItem(at: pairingURL)
|
|
||||||
}
|
|
||||||
try FileManager.default.copyItem(at: src, to: pairingURL)
|
|
||||||
try FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: pairingURL.path)
|
|
||||||
notify("Pairing File Added", "Your device is ready. Tap Reload to fetch info.")
|
notify("Pairing File Added", "Your device is ready. Tap Reload to fetch info.")
|
||||||
mgr.initAndLoad()
|
mgr.initAndLoad()
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,222 @@
|
||||||
//
|
//
|
||||||
// Created by s s on 2025/7/9.
|
// Created by s s on 2025/7/9.
|
||||||
//
|
//
|
||||||
|
import Foundation
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
enum PairingFileStore {
|
||||||
|
static let fileName = "rp_pairing_file.plist"
|
||||||
|
private static let legacyFileName = "pairingFile.plist"
|
||||||
|
static let supportedContentTypes: [UTType] = [
|
||||||
|
UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!,
|
||||||
|
UTType(filenameExtension: "mobiledevicepair", conformingTo: .data)!,
|
||||||
|
.propertyList
|
||||||
|
]
|
||||||
|
|
||||||
|
static var url: URL {
|
||||||
|
URL.documentsDirectory.appendingPathComponent(fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static var legacyURL: URL {
|
||||||
|
URL.documentsDirectory.appendingPathComponent(legacyFileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
static func prepareURL(fileManager: FileManager = .default) -> URL {
|
||||||
|
let destination = url
|
||||||
|
guard !fileManager.fileExists(atPath: destination.path),
|
||||||
|
fileManager.fileExists(atPath: legacyURL.path) else {
|
||||||
|
return destination
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
try fileManager.moveItem(at: legacyURL, to: destination)
|
||||||
|
} catch {
|
||||||
|
if let data = try? Data(contentsOf: legacyURL) {
|
||||||
|
try? data.write(to: destination, options: .atomic)
|
||||||
|
try? fileManager.removeItem(at: legacyURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return destination
|
||||||
|
}
|
||||||
|
|
||||||
|
static func replace(with sourceURL: URL, fileManager: FileManager = .default) throws {
|
||||||
|
let destination = prepareURL(fileManager: fileManager)
|
||||||
|
if fileManager.fileExists(atPath: destination.path) {
|
||||||
|
try fileManager.removeItem(at: destination)
|
||||||
|
}
|
||||||
|
if fileManager.fileExists(atPath: legacyURL.path) {
|
||||||
|
try? fileManager.removeItem(at: legacyURL)
|
||||||
|
}
|
||||||
|
try fileManager.copyItem(at: sourceURL, to: destination)
|
||||||
|
try? fileManager.setAttributes([.posixPermissions: 0o600], ofItemAtPath: destination.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func importFromPicker(_ sourceURL: URL, fileManager: FileManager = .default) throws {
|
||||||
|
let accessing = sourceURL.startAccessingSecurityScopedResource()
|
||||||
|
defer {
|
||||||
|
if accessing {
|
||||||
|
sourceURL.stopAccessingSecurityScopedResource()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard fileManager.fileExists(atPath: sourceURL.path) else {
|
||||||
|
throw CocoaError(.fileNoSuchFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
try replace(with: sourceURL, fileManager: fileManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func remove(fileManager: FileManager = .default) throws {
|
||||||
|
let destination = prepareURL(fileManager: fileManager)
|
||||||
|
if fileManager.fileExists(atPath: destination.path) {
|
||||||
|
try fileManager.removeItem(at: destination)
|
||||||
|
}
|
||||||
|
if fileManager.fileExists(atPath: legacyURL.path) {
|
||||||
|
try? fileManager.removeItem(at: legacyURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ScriptResource {
|
||||||
|
let resourceName: String
|
||||||
|
let fileName: String
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ScriptStore {
|
||||||
|
static let directoryName = "scripts"
|
||||||
|
static let assignmentKey = UserDefaults.Keys.bundleScriptMap
|
||||||
|
static let favoriteAppNamesSuiteName = "group.com.stik.sj"
|
||||||
|
static let favoriteAppNamesKey = "favoriteAppNames"
|
||||||
|
static let defaultScriptName = UserDefaults.Keys.defaultScriptNameValue
|
||||||
|
static let bundledResources: [ScriptResource] = [
|
||||||
|
ScriptResource(resourceName: "attachDetach", fileName: "attachDetach.js"),
|
||||||
|
ScriptResource(resourceName: "maciOS", fileName: "maciOS.js"),
|
||||||
|
ScriptResource(resourceName: "universal", fileName: "universal.js"),
|
||||||
|
ScriptResource(resourceName: "Geode", fileName: "Geode.js"),
|
||||||
|
ScriptResource(resourceName: "manic", fileName: "manic.js"),
|
||||||
|
ScriptResource(resourceName: "UTM-Dolphin", fileName: "UTM-Dolphin.js")
|
||||||
|
]
|
||||||
|
|
||||||
|
static var directoryURL: URL {
|
||||||
|
URL.documentsDirectory.appendingPathComponent(directoryName)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
static func prepareDirectory(fileManager: FileManager = .default) throws -> URL {
|
||||||
|
let directory = directoryURL
|
||||||
|
var isDirectory: ObjCBool = false
|
||||||
|
let exists = fileManager.fileExists(atPath: directory.path, isDirectory: &isDirectory)
|
||||||
|
if exists && !isDirectory.boolValue {
|
||||||
|
try fileManager.removeItem(at: directory)
|
||||||
|
}
|
||||||
|
if !exists || !isDirectory.boolValue {
|
||||||
|
try fileManager.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||||
|
}
|
||||||
|
try ensureBundledScripts(in: directory, fileManager: fileManager)
|
||||||
|
return directory
|
||||||
|
}
|
||||||
|
|
||||||
|
static func scriptURL(named scriptName: String, fileManager: FileManager = .default) throws -> URL {
|
||||||
|
let directory = try prepareDirectory(fileManager: fileManager)
|
||||||
|
return directory.appendingPathComponent(scriptName)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func assignedScriptName(for bundleID: String, defaults: UserDefaults = .standard) -> String? {
|
||||||
|
assignedScriptMap(defaults: defaults)[bundleID]
|
||||||
|
}
|
||||||
|
|
||||||
|
static func updateAssignedScriptName(_ scriptName: String?, for bundleID: String, defaults: UserDefaults = .standard) {
|
||||||
|
var mapping = assignedScriptMap(defaults: defaults)
|
||||||
|
if let scriptName {
|
||||||
|
mapping[bundleID] = scriptName
|
||||||
|
} else {
|
||||||
|
mapping.removeValue(forKey: bundleID)
|
||||||
|
}
|
||||||
|
defaults.set(mapping, forKey: assignmentKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func preferredScript(for bundleID: String, fileManager: FileManager = .default) -> (data: Data, name: String)? {
|
||||||
|
assignedScript(for: bundleID, fileManager: fileManager) ?? autoScript(for: bundleID, fileManager: fileManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func favoriteAppName(for bundleID: String, defaults: UserDefaults? = UserDefaults(suiteName: favoriteAppNamesSuiteName)) -> String? {
|
||||||
|
let names = defaults?.dictionary(forKey: favoriteAppNamesKey) as? [String: String]
|
||||||
|
return names?[bundleID]
|
||||||
|
}
|
||||||
|
|
||||||
|
static func autoScriptResource(for appName: String) -> ScriptResource? {
|
||||||
|
switch appName {
|
||||||
|
case "maciOS":
|
||||||
|
return ScriptResource(resourceName: "maciOS", fileName: "maciOS.js")
|
||||||
|
case "Amethyst", "MeloNX", "XeniOS", "MeloCafe":
|
||||||
|
return ScriptResource(resourceName: "universal", fileName: "universal.js")
|
||||||
|
case "Geode":
|
||||||
|
return ScriptResource(resourceName: "Geode", fileName: "Geode.js")
|
||||||
|
case "Manic EMU":
|
||||||
|
return ScriptResource(resourceName: "manic", fileName: "manic.js")
|
||||||
|
case "UTM", "DolphiniOS", "Flycast":
|
||||||
|
return ScriptResource(resourceName: "UTM-Dolphin", fileName: "UTM-Dolphin.js")
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func ensureBundledScripts(in directory: URL, fileManager: FileManager) throws {
|
||||||
|
for resource in bundledResources {
|
||||||
|
guard let bundleURL = Bundle.main.url(forResource: resource.resourceName, withExtension: "js") else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let destination = directory.appendingPathComponent(resource.fileName)
|
||||||
|
if !fileManager.fileExists(atPath: destination.path) {
|
||||||
|
try fileManager.copyItem(at: bundleURL, to: destination)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func assignedScript(for bundleID: String, fileManager: FileManager) -> (data: Data, name: String)? {
|
||||||
|
guard let scriptName = assignedScriptName(for: bundleID) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let scriptURL = try? scriptURL(named: scriptName, fileManager: fileManager),
|
||||||
|
let data = try? Data(contentsOf: scriptURL) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return (data, scriptName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func autoScript(for bundleID: String, fileManager: FileManager) -> (data: Data, name: String)? {
|
||||||
|
guard ProcessInfo.processInfo.hasTXM else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard #available(iOS 26, *) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let appName = (try? JITEnableContext.shared.getAppList()[bundleID]) ?? favoriteAppName(for: bundleID)
|
||||||
|
guard let appName,
|
||||||
|
let resource = autoScriptResource(for: appName) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if let scriptURL = try? scriptURL(named: resource.fileName, fileManager: fileManager),
|
||||||
|
let data = try? Data(contentsOf: scriptURL) {
|
||||||
|
return (data, resource.fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let bundleURL = Bundle.main.url(forResource: resource.resourceName, withExtension: "js"),
|
||||||
|
let data = try? Data(contentsOf: bundleURL) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return (data, resource.fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func assignedScriptMap(defaults: UserDefaults) -> [String: String] {
|
||||||
|
defaults.dictionary(forKey: assignmentKey) as? [String: String] ?? [:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension FileManager {
|
extension FileManager {
|
||||||
func filePath(atPath path: String, withLength length: Int) -> String? {
|
func filePath(atPath path: String, withLength length: Int) -> String? {
|
||||||
|
|
@ -21,11 +236,15 @@ extension UIDocumentPickerViewController {
|
||||||
|
|
||||||
extension Notification.Name {
|
extension Notification.Name {
|
||||||
static let pairingFileImported = Notification.Name("PairingFileImported")
|
static let pairingFileImported = Notification.Name("PairingFileImported")
|
||||||
|
static let intentJSScriptReady = Notification.Name("intentJSScriptReady")
|
||||||
}
|
}
|
||||||
|
|
||||||
extension UserDefaults {
|
extension UserDefaults {
|
||||||
enum Keys {
|
enum Keys {
|
||||||
/// Forces the app to treat the current device as TXM-capable so scripts always run.
|
/// Forces the app to treat the current device as TXM-capable so scripts always run.
|
||||||
static let txmOverride = "overrideTXMForScripts"
|
static let txmOverride = "overrideTXMForScripts"
|
||||||
|
static let bundleScriptMap = "BundleScriptMap"
|
||||||
|
static let defaultScriptName = "DefaultScriptName"
|
||||||
|
static let defaultScriptNameValue = "attachDetach.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
805
StikJIT/Utilities/IdeviceFFIBridge.swift
Normal file
805
StikJIT/Utilities/IdeviceFFIBridge.swift
Normal file
|
|
@ -0,0 +1,805 @@
|
||||||
|
//
|
||||||
|
// IdeviceFFIBridge.swift
|
||||||
|
// StikDebug
|
||||||
|
//
|
||||||
|
// Created by Stephen on 2026/3/30.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import idevice
|
||||||
|
|
||||||
|
private enum IdeviceBridge {
|
||||||
|
static let processQueue = DispatchQueue(label: "com.stikdebug.processInspector", qos: .userInitiated)
|
||||||
|
|
||||||
|
static func makeError(
|
||||||
|
domain: String = "StikDebug",
|
||||||
|
code: Int = -1,
|
||||||
|
message: String
|
||||||
|
) -> NSError {
|
||||||
|
NSError(
|
||||||
|
domain: domain,
|
||||||
|
code: code,
|
||||||
|
userInfo: [NSLocalizedDescriptionKey: message]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func string(from cString: UnsafePointer<CChar>?) -> String? {
|
||||||
|
guard let cString else { return nil }
|
||||||
|
return String(validatingUTF8: cString)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func consumeFFIError(
|
||||||
|
_ ffiError: UnsafeMutablePointer<IdeviceFfiError>?,
|
||||||
|
fallback: String,
|
||||||
|
domain: String = "StikDebug"
|
||||||
|
) -> NSError {
|
||||||
|
guard let ffiError else {
|
||||||
|
return makeError(domain: domain, message: fallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
let code = Int(ffiError.pointee.code)
|
||||||
|
let message = string(from: ffiError.pointee.message) ?? fallback
|
||||||
|
idevice_error_free(ffiError)
|
||||||
|
return makeError(domain: domain, code: code, message: message)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func withTunnelHandles<T>(
|
||||||
|
for context: JITEnableContext,
|
||||||
|
_ body: (OpaquePointer, OpaquePointer) throws -> T
|
||||||
|
) throws -> T {
|
||||||
|
let handles = try activeTunnelHandles(for: context)
|
||||||
|
return try body(handles.adapter, handles.handshake)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func connectClient(
|
||||||
|
fallback: String,
|
||||||
|
missingClientMessage: String,
|
||||||
|
domain: String = "StikDebug",
|
||||||
|
connect: (UnsafeMutablePointer<OpaquePointer?>) -> UnsafeMutablePointer<IdeviceFfiError>?
|
||||||
|
) throws -> OpaquePointer {
|
||||||
|
var client: OpaquePointer?
|
||||||
|
if let ffiError = connect(&client) {
|
||||||
|
throw consumeFFIError(ffiError, fallback: fallback, domain: domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let client else {
|
||||||
|
throw makeError(domain: domain, message: missingClientMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
static func withConnectedClient<T>(
|
||||||
|
fallback: String,
|
||||||
|
missingClientMessage: String,
|
||||||
|
domain: String = "StikDebug",
|
||||||
|
connect: (UnsafeMutablePointer<OpaquePointer?>) -> UnsafeMutablePointer<IdeviceFfiError>?,
|
||||||
|
cleanup: (OpaquePointer) -> Void,
|
||||||
|
_ body: (OpaquePointer) throws -> T
|
||||||
|
) throws -> T {
|
||||||
|
let client = try connectClient(
|
||||||
|
fallback: fallback,
|
||||||
|
missingClientMessage: missingClientMessage,
|
||||||
|
domain: domain,
|
||||||
|
connect: connect
|
||||||
|
)
|
||||||
|
defer { cleanup(client) }
|
||||||
|
return try body(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func plistDictionaries(adapter: OpaquePointer, handshake: OpaquePointer) throws -> [[String: Any]] {
|
||||||
|
try withConnectedClient(
|
||||||
|
fallback: "Failed to connect to installation proxy",
|
||||||
|
missingClientMessage: "Installation proxy client was not created",
|
||||||
|
connect: { installation_proxy_connect_rsd(adapter, handshake, $0) },
|
||||||
|
cleanup: { installation_proxy_client_free($0) }
|
||||||
|
) { client in
|
||||||
|
var rawApps: UnsafeMutableRawPointer?
|
||||||
|
var count = 0
|
||||||
|
if let ffiError = installation_proxy_get_apps(client, nil, nil, 0, &rawApps, &count) {
|
||||||
|
throw consumeFFIError(ffiError, fallback: "Failed to fetch installed apps")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let rawApps, count > 0 else { return [] }
|
||||||
|
|
||||||
|
let apps = rawApps.assumingMemoryBound(to: plist_t?.self)
|
||||||
|
defer {
|
||||||
|
for index in 0..<count {
|
||||||
|
plist_free(apps[index])
|
||||||
|
}
|
||||||
|
idevice_data_free(
|
||||||
|
rawApps.assumingMemoryBound(to: UInt8.self),
|
||||||
|
UInt(count * MemoryLayout<plist_t?>.stride)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var dictionaries: [[String: Any]] = []
|
||||||
|
dictionaries.reserveCapacity(count)
|
||||||
|
|
||||||
|
for index in 0..<count {
|
||||||
|
var binaryPlist: UnsafeMutablePointer<CChar>?
|
||||||
|
var binaryLength: UInt32 = 0
|
||||||
|
let app = apps[index]
|
||||||
|
|
||||||
|
guard plist_to_bin(app, &binaryPlist, &binaryLength) == PLIST_ERR_SUCCESS,
|
||||||
|
let binaryPlist,
|
||||||
|
binaryLength > 0 else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = Data(bytes: binaryPlist, count: Int(binaryLength))
|
||||||
|
plist_mem_free(binaryPlist)
|
||||||
|
|
||||||
|
guard let plist = try? PropertyListSerialization.propertyList(from: data, format: nil),
|
||||||
|
let dictionary = plist as? [String: Any] else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dictionaries.append(dictionary)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dictionaries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func appName(from dictionary: [String: Any]) -> String {
|
||||||
|
if let displayName = dictionary["CFBundleDisplayName"] as? String, !displayName.isEmpty {
|
||||||
|
return displayName
|
||||||
|
}
|
||||||
|
if let name = dictionary["CFBundleName"] as? String, !name.isEmpty {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
static func hasGetTaskAllow(_ dictionary: [String: Any]) -> Bool {
|
||||||
|
guard let entitlements = dictionary["Entitlements"] as? [String: Any] else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if let flag = entitlements["get-task-allow"] as? Bool {
|
||||||
|
return flag
|
||||||
|
}
|
||||||
|
|
||||||
|
if let flag = entitlements["get-task-allow"] as? NSNumber {
|
||||||
|
return flag.boolValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
static func isHiddenSystemApp(_ dictionary: [String: Any]) -> Bool {
|
||||||
|
guard let applicationType = dictionary["ApplicationType"] as? String,
|
||||||
|
applicationType == "System" || applicationType == "HiddenSystemApp" else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if let isHidden = dictionary["IsHidden"] as? Bool, isHidden {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if let isHidden = dictionary["IsHidden"] as? NSNumber, isHidden.boolValue {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let tags = dictionary["SBAppTags"] as? [String] else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags.contains("hidden") || tags.contains("hidden-system-app")
|
||||||
|
}
|
||||||
|
|
||||||
|
static func appDictionary(
|
||||||
|
adapter: OpaquePointer,
|
||||||
|
handshake: OpaquePointer,
|
||||||
|
requireGetTaskAllow: Bool,
|
||||||
|
filter: (([String: Any]) -> Bool)? = nil
|
||||||
|
) throws -> [String: String] {
|
||||||
|
let dictionaries = try plistDictionaries(adapter: adapter, handshake: handshake)
|
||||||
|
var result: [String: String] = [:]
|
||||||
|
result.reserveCapacity(dictionaries.count)
|
||||||
|
|
||||||
|
for dictionary in dictionaries {
|
||||||
|
if requireGetTaskAllow && !hasGetTaskAllow(dictionary) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if let filter, !filter(dictionary) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let bundleID = dictionary["CFBundleIdentifier"] as? String,
|
||||||
|
!bundleID.isEmpty else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
result[bundleID] = appName(from: dictionary)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
static func activeTunnelHandles(for context: JITEnableContext) throws -> (adapter: OpaquePointer, handshake: OpaquePointer) {
|
||||||
|
try context.ensureTunnel()
|
||||||
|
|
||||||
|
guard let adapterHandle = context.adapterHandle,
|
||||||
|
let handshakeHandle = context.handshakeHandle else {
|
||||||
|
throw makeError(message: "Tunnel is not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
return (adapterHandle, handshakeHandle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension JITEnableContext {
|
||||||
|
func getMountedDeviceCount() throws -> Int {
|
||||||
|
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
|
||||||
|
try IdeviceBridge.withConnectedClient(
|
||||||
|
fallback: "Failed to connect to image mounter",
|
||||||
|
missingClientMessage: "Image mounter client was not created",
|
||||||
|
connect: { image_mounter_connect_rsd(adapter, handshake, $0) },
|
||||||
|
cleanup: { image_mounter_free($0) }
|
||||||
|
) { client in
|
||||||
|
var devices: UnsafeMutablePointer<plist_t?>?
|
||||||
|
var deviceCount = 0
|
||||||
|
if let ffiError = image_mounter_copy_devices(client, &devices, &deviceCount) {
|
||||||
|
throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to fetch mounted devices")
|
||||||
|
}
|
||||||
|
|
||||||
|
if let devices {
|
||||||
|
for index in 0..<deviceCount {
|
||||||
|
plist_free(devices[index])
|
||||||
|
}
|
||||||
|
idevice_data_free(
|
||||||
|
UnsafeMutableRawPointer(devices).assumingMemoryBound(to: UInt8.self),
|
||||||
|
UInt(deviceCount * MemoryLayout<plist_t?>.stride)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return deviceCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mountPersonalDDI(withImagePath imagePath: String, trustcachePath: String, manifestPath: String) throws {
|
||||||
|
guard FileManager.default.fileExists(atPath: imagePath),
|
||||||
|
FileManager.default.fileExists(atPath: trustcachePath),
|
||||||
|
FileManager.default.fileExists(atPath: manifestPath) else {
|
||||||
|
throw IdeviceBridge.makeError(code: 1, message: "Failed to read one or more files")
|
||||||
|
}
|
||||||
|
|
||||||
|
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
|
||||||
|
try IdeviceBridge.withConnectedClient(
|
||||||
|
fallback: "Failed to connect to lockdownd",
|
||||||
|
missingClientMessage: "Lockdownd client was not created",
|
||||||
|
connect: { lockdownd_connect_rsd(adapter, handshake, $0) },
|
||||||
|
cleanup: { lockdownd_client_free($0) }
|
||||||
|
) { lockdownClient in
|
||||||
|
var uniqueChipIDPlist: plist_t?
|
||||||
|
if let ffiError = lockdownd_get_value(lockdownClient, "UniqueChipID", nil, &uniqueChipIDPlist) {
|
||||||
|
throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to query UniqueChipID")
|
||||||
|
}
|
||||||
|
|
||||||
|
if let uniqueChipIDPlist {
|
||||||
|
plist_free(uniqueChipIDPlist)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try IdeviceBridge.withConnectedClient(
|
||||||
|
fallback: "Failed to connect to image mounter",
|
||||||
|
missingClientMessage: "Image mounter client was not created",
|
||||||
|
connect: { image_mounter_connect_rsd(adapter, handshake, $0) },
|
||||||
|
cleanup: { image_mounter_free($0) }
|
||||||
|
) { _ in
|
||||||
|
throw IdeviceBridge.makeError(
|
||||||
|
code: 10,
|
||||||
|
message: "mount_personalized not yet available over RSD tunnels"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchAllProfiles() throws -> [Data] {
|
||||||
|
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
|
||||||
|
try IdeviceBridge.withConnectedClient(
|
||||||
|
fallback: "Failed to connect to misagent",
|
||||||
|
missingClientMessage: "Misagent client was not created",
|
||||||
|
domain: "profiles",
|
||||||
|
connect: { misagent_connect_rsd(adapter, handshake, $0) },
|
||||||
|
cleanup: { misagent_client_free($0) }
|
||||||
|
) { misagentClient in
|
||||||
|
var profilePointers: UnsafeMutablePointer<UnsafeMutablePointer<UInt8>?>?
|
||||||
|
var profileLengths: UnsafeMutablePointer<Int>?
|
||||||
|
var profileCount = 0
|
||||||
|
|
||||||
|
if let ffiError = misagent_copy_all(misagentClient, &profilePointers, &profileLengths, &profileCount) {
|
||||||
|
throw IdeviceBridge.consumeFFIError(
|
||||||
|
ffiError,
|
||||||
|
fallback: "Failed to fetch provisioning profiles",
|
||||||
|
domain: "profiles"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer {
|
||||||
|
if let profilePointers, let profileLengths {
|
||||||
|
misagent_free_profiles(profilePointers, profileLengths, profileCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let profilePointers, let profileLengths else { return [] }
|
||||||
|
|
||||||
|
var result: [Data] = []
|
||||||
|
result.reserveCapacity(profileCount)
|
||||||
|
|
||||||
|
for index in 0..<profileCount {
|
||||||
|
guard let bytes = profilePointers[index] else { continue }
|
||||||
|
result.append(Data(bytes: bytes, count: profileLengths[index]))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeProfile(withUUID uuid: String) throws {
|
||||||
|
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
|
||||||
|
try IdeviceBridge.withConnectedClient(
|
||||||
|
fallback: "Failed to connect to misagent",
|
||||||
|
missingClientMessage: "Misagent client was not created",
|
||||||
|
domain: "profiles",
|
||||||
|
connect: { misagent_connect_rsd(adapter, handshake, $0) },
|
||||||
|
cleanup: { misagent_client_free($0) }
|
||||||
|
) { misagentClient in
|
||||||
|
if let ffiError = misagent_remove(misagentClient, uuid) {
|
||||||
|
throw IdeviceBridge.consumeFFIError(
|
||||||
|
ffiError,
|
||||||
|
fallback: "Failed to remove provisioning profile",
|
||||||
|
domain: "profiles"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addProfile(_ profile: Data) throws {
|
||||||
|
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
|
||||||
|
try IdeviceBridge.withConnectedClient(
|
||||||
|
fallback: "Failed to connect to misagent",
|
||||||
|
missingClientMessage: "Misagent client was not created",
|
||||||
|
domain: "profiles",
|
||||||
|
connect: { misagent_connect_rsd(adapter, handshake, $0) },
|
||||||
|
cleanup: { misagent_client_free($0) }
|
||||||
|
) { misagentClient in
|
||||||
|
let ffiError = profile.withUnsafeBytes { rawBuffer in
|
||||||
|
misagent_install(
|
||||||
|
misagentClient,
|
||||||
|
rawBuffer.bindMemory(to: UInt8.self).baseAddress,
|
||||||
|
profile.count
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let ffiError {
|
||||||
|
throw IdeviceBridge.consumeFFIError(
|
||||||
|
ffiError,
|
||||||
|
fallback: "Failed to add provisioning profile",
|
||||||
|
domain: "profiles"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchProcessList() throws -> [NSDictionary] {
|
||||||
|
try IdeviceBridge.processQueue.sync {
|
||||||
|
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
|
||||||
|
try IdeviceBridge.withConnectedClient(
|
||||||
|
fallback: "Unable to open AppService",
|
||||||
|
missingClientMessage: "AppService client was not created",
|
||||||
|
connect: { app_service_connect_rsd(adapter, handshake, $0) },
|
||||||
|
cleanup: { app_service_free($0) }
|
||||||
|
) { appService in
|
||||||
|
var processes: UnsafeMutablePointer<ProcessTokenC>?
|
||||||
|
var count = UInt(0)
|
||||||
|
if let ffiError = app_service_list_processes(appService, &processes, &count) {
|
||||||
|
throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to list processes")
|
||||||
|
}
|
||||||
|
|
||||||
|
defer {
|
||||||
|
if let processes {
|
||||||
|
app_service_free_process_list(processes, count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let processes else { return [] }
|
||||||
|
|
||||||
|
var result: [NSDictionary] = []
|
||||||
|
result.reserveCapacity(Int(count))
|
||||||
|
|
||||||
|
for index in 0..<Int(count) {
|
||||||
|
let process = processes[index]
|
||||||
|
var dictionary: [String: Any] = ["pid": NSNumber(value: process.pid)]
|
||||||
|
if let executableURL = IdeviceBridge.string(from: process.executable_url) {
|
||||||
|
dictionary["path"] = executableURL
|
||||||
|
}
|
||||||
|
result.append(dictionary as NSDictionary)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendSignal(_ signal: Int32, toProcessWithPID pid: Int32) throws {
|
||||||
|
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
|
||||||
|
try IdeviceBridge.withConnectedClient(
|
||||||
|
fallback: "Unable to open AppService",
|
||||||
|
missingClientMessage: "AppService client was not created",
|
||||||
|
connect: { app_service_connect_rsd(adapter, handshake, $0) },
|
||||||
|
cleanup: { app_service_free($0) }
|
||||||
|
) { appService in
|
||||||
|
var response: UnsafeMutablePointer<SignalResponseC>?
|
||||||
|
let ffiError = app_service_send_signal(appService, UInt32(pid), UInt32(signal), &response)
|
||||||
|
defer {
|
||||||
|
if let response {
|
||||||
|
app_service_free_signal_response(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let ffiError {
|
||||||
|
throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to send signal \(signal) to process")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func killProcess(withPID pid: Int32) throws {
|
||||||
|
try sendSignal(Int32(SIGKILL), toProcessWithPID: pid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAppList() throws -> [String: String] {
|
||||||
|
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
|
||||||
|
try IdeviceBridge.appDictionary(
|
||||||
|
adapter: adapter,
|
||||||
|
handshake: handshake,
|
||||||
|
requireGetTaskAllow: true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAllApps() throws -> [String: String] {
|
||||||
|
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
|
||||||
|
try IdeviceBridge.appDictionary(
|
||||||
|
adapter: adapter,
|
||||||
|
handshake: handshake,
|
||||||
|
requireGetTaskAllow: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHiddenSystemApps() throws -> [String: String] {
|
||||||
|
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
|
||||||
|
try IdeviceBridge.appDictionary(
|
||||||
|
adapter: adapter,
|
||||||
|
handshake: handshake,
|
||||||
|
requireGetTaskAllow: false,
|
||||||
|
filter: IdeviceBridge.isHiddenSystemApp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSideloadedApps() throws -> [NSDictionary] {
|
||||||
|
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
|
||||||
|
try IdeviceBridge.plistDictionaries(adapter: adapter, handshake: handshake)
|
||||||
|
.filter { $0["ProfileValidated"] != nil }
|
||||||
|
.map { $0 as NSDictionary }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAppIcon(withBundleId bundleId: String) throws -> UIImage {
|
||||||
|
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
|
||||||
|
try IdeviceBridge.withConnectedClient(
|
||||||
|
fallback: "Failed to connect to SpringBoard Services",
|
||||||
|
missingClientMessage: "SpringBoard Services client was not created",
|
||||||
|
connect: { springboard_services_connect_rsd(adapter, handshake, $0) },
|
||||||
|
cleanup: { springboard_services_free($0) }
|
||||||
|
) { client in
|
||||||
|
var rawIconData: UnsafeMutableRawPointer?
|
||||||
|
var rawIconLength = 0
|
||||||
|
if let ffiError = springboard_services_get_icon(client, bundleId, &rawIconData, &rawIconLength) {
|
||||||
|
throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to get app icon")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let rawIconData, rawIconLength > 0 else {
|
||||||
|
throw IdeviceBridge.makeError(message: "App icon data was empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
defer { free(rawIconData) }
|
||||||
|
|
||||||
|
let data = Data(bytes: rawIconData, count: rawIconLength)
|
||||||
|
guard let image = UIImage(data: data) else {
|
||||||
|
throw IdeviceBridge.makeError(message: "Failed to decode app icon image")
|
||||||
|
}
|
||||||
|
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ideviceInfoInit() throws -> OpaquePointer {
|
||||||
|
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
|
||||||
|
try IdeviceBridge.connectClient(
|
||||||
|
fallback: "Failed to connect to lockdownd",
|
||||||
|
missingClientMessage: "Lockdownd client was not created",
|
||||||
|
domain: "profiles",
|
||||||
|
connect: { lockdownd_connect_rsd(adapter, handshake, $0) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ideviceInfoGetXML(withLockdownClient lockdownClient: OpaquePointer?) throws -> UnsafeMutablePointer<CChar>? {
|
||||||
|
guard let lockdownClient else { return nil }
|
||||||
|
|
||||||
|
var plistObject: plist_t?
|
||||||
|
if let ffiError = lockdownd_get_value(lockdownClient, nil, nil, &plistObject) {
|
||||||
|
throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to fetch device info")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let plistObject else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
defer { plist_free(plistObject) }
|
||||||
|
|
||||||
|
var xml: UnsafeMutablePointer<CChar>?
|
||||||
|
var xmlLength: UInt32 = 0
|
||||||
|
guard plist_to_xml(plistObject, &xml, &xmlLength) == PLIST_ERR_SUCCESS,
|
||||||
|
let xml,
|
||||||
|
xmlLength > 0 else {
|
||||||
|
throw IdeviceBridge.makeError(message: "Failed to serialize device info plist")
|
||||||
|
}
|
||||||
|
|
||||||
|
return xml
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchDeviceProcessList(_ error: NSErrorPointer) -> [NSDictionary]? {
|
||||||
|
do {
|
||||||
|
return try JITEnableContext.shared.fetchProcessList()
|
||||||
|
} catch let nsError as NSError {
|
||||||
|
error?.pointee = nsError
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func KillDeviceProcess(_ pid: Int32, _ error: NSErrorPointer) -> Bool {
|
||||||
|
do {
|
||||||
|
try JITEnableContext.shared.killProcess(withPID: pid)
|
||||||
|
return true
|
||||||
|
} catch let nsError as NSError {
|
||||||
|
error?.pointee = nsError
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ProcessInfoEntry: Identifiable {
|
||||||
|
let pid: Int
|
||||||
|
private let rawPath: String
|
||||||
|
let bundleID: String?
|
||||||
|
let name: String?
|
||||||
|
|
||||||
|
init?(dictionary: NSDictionary) {
|
||||||
|
guard let pidNumber = dictionary["pid"] as? NSNumber else { return nil }
|
||||||
|
pid = pidNumber.intValue
|
||||||
|
rawPath = dictionary["path"] as? String ?? "Unknown"
|
||||||
|
bundleID = dictionary["bundleID"] as? String
|
||||||
|
name = dictionary["name"] as? String
|
||||||
|
}
|
||||||
|
|
||||||
|
static func currentEntries(_ error: NSErrorPointer = nil) -> [ProcessInfoEntry] {
|
||||||
|
let entries = FetchDeviceProcessList(error) ?? []
|
||||||
|
return entries.compactMap(Self.init(dictionary:))
|
||||||
|
}
|
||||||
|
|
||||||
|
var id: Int { pid }
|
||||||
|
|
||||||
|
var executablePath: String {
|
||||||
|
rawPath.replacingOccurrences(of: "file://", with: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayName: String {
|
||||||
|
if let name, !name.isEmpty {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
if let bundleID, !bundleID.isEmpty {
|
||||||
|
return bundleID
|
||||||
|
}
|
||||||
|
if let component = executablePath.split(separator: "/").last {
|
||||||
|
return String(component)
|
||||||
|
}
|
||||||
|
return "Process \(pid)"
|
||||||
|
}
|
||||||
|
|
||||||
|
var stableIdentifier: String {
|
||||||
|
if let bundleID, !bundleID.isEmpty {
|
||||||
|
return bundleID
|
||||||
|
}
|
||||||
|
return displayName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objcMembers
|
||||||
|
final class CMSDecoderHelper: NSObject {
|
||||||
|
static func decodeCMSData(_ cmsData: Data) throws -> Data {
|
||||||
|
guard !cmsData.isEmpty else {
|
||||||
|
throw IdeviceBridge.makeError(
|
||||||
|
domain: NSCocoaErrorDomain,
|
||||||
|
code: NSURLErrorBadURL,
|
||||||
|
message: "Invalid or empty CMS payload"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let xmlStart = Data("<?xml".utf8)
|
||||||
|
let plistEnd = Data("</plist>".utf8)
|
||||||
|
let binaryMagic = Data("bplist00".utf8)
|
||||||
|
|
||||||
|
if let startRange = cmsData.range(of: xmlStart),
|
||||||
|
let endRange = cmsData.range(of: plistEnd, options: [], in: startRange.lowerBound..<cmsData.endIndex) {
|
||||||
|
return cmsData[startRange.lowerBound..<endRange.upperBound]
|
||||||
|
}
|
||||||
|
|
||||||
|
if let binaryRange = cmsData.range(of: binaryMagic) {
|
||||||
|
return cmsData[binaryRange.lowerBound..<cmsData.endIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
throw IdeviceBridge.makeError(
|
||||||
|
domain: NSCocoaErrorDomain,
|
||||||
|
code: NSFileReadUnknownError,
|
||||||
|
message: "Unable to extract plist from CMS payload"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum LocationSimulationStatus {
|
||||||
|
static let ok: Int32 = 0
|
||||||
|
static let invalidIP: Int32 = 1
|
||||||
|
static let pairingRead: Int32 = 2
|
||||||
|
static let providerCreate: Int32 = 3
|
||||||
|
static let remoteServer: Int32 = 9
|
||||||
|
static let locationSimulation: Int32 = 10
|
||||||
|
static let locationSet: Int32 = 11
|
||||||
|
static let locationClear: Int32 = 12
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum LocationSimulationState {
|
||||||
|
static var adapter: OpaquePointer?
|
||||||
|
static var handshake: OpaquePointer?
|
||||||
|
static var remoteServer: OpaquePointer?
|
||||||
|
static var locationSimulation: OpaquePointer?
|
||||||
|
|
||||||
|
static func cleanup() {
|
||||||
|
if let locationSimulation {
|
||||||
|
location_simulation_free(locationSimulation)
|
||||||
|
self.locationSimulation = nil
|
||||||
|
}
|
||||||
|
if let remoteServer {
|
||||||
|
remote_server_free(remoteServer)
|
||||||
|
self.remoteServer = nil
|
||||||
|
}
|
||||||
|
if let handshake {
|
||||||
|
rsd_handshake_free(handshake)
|
||||||
|
self.handshake = nil
|
||||||
|
}
|
||||||
|
if let adapter {
|
||||||
|
adapter_free(adapter)
|
||||||
|
self.adapter = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func simulate_location(_ deviceIP: String, _ latitude: Double, _ longitude: Double, _ pairingFile: String) -> Int32 {
|
||||||
|
if let locationSimulation = LocationSimulationState.locationSimulation {
|
||||||
|
if let ffiError = location_simulation_set(locationSimulation, latitude, longitude) {
|
||||||
|
idevice_error_free(ffiError)
|
||||||
|
LocationSimulationState.cleanup()
|
||||||
|
} else {
|
||||||
|
return LocationSimulationStatus.ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var address = sockaddr_in()
|
||||||
|
address.sin_family = sa_family_t(AF_INET)
|
||||||
|
address.sin_port = in_port_t(49152).bigEndian
|
||||||
|
|
||||||
|
let inetResult = deviceIP.withCString { inet_pton(AF_INET, $0, &address.sin_addr) }
|
||||||
|
guard inetResult == 1 else {
|
||||||
|
return LocationSimulationStatus.invalidIP
|
||||||
|
}
|
||||||
|
|
||||||
|
var pairingHandle: OpaquePointer?
|
||||||
|
let pairingError = pairingFile.withCString { rp_pairing_file_read($0, &pairingHandle) }
|
||||||
|
if let pairingError {
|
||||||
|
idevice_error_free(pairingError)
|
||||||
|
return LocationSimulationStatus.pairingRead
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let pairingHandle else {
|
||||||
|
return LocationSimulationStatus.pairingRead
|
||||||
|
}
|
||||||
|
|
||||||
|
defer { rp_pairing_file_free(pairingHandle) }
|
||||||
|
|
||||||
|
let providerError = withUnsafePointer(to: &address) { pointer in
|
||||||
|
pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) {
|
||||||
|
tunnel_create_rppairing(
|
||||||
|
$0,
|
||||||
|
socklen_t(MemoryLayout<sockaddr_in>.stride),
|
||||||
|
"StikDebugLocation",
|
||||||
|
pairingHandle,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
&LocationSimulationState.adapter,
|
||||||
|
&LocationSimulationState.handshake
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let providerError {
|
||||||
|
idevice_error_free(providerError)
|
||||||
|
LocationSimulationState.cleanup()
|
||||||
|
return LocationSimulationStatus.providerCreate
|
||||||
|
}
|
||||||
|
|
||||||
|
let remoteServerError = remote_server_connect_rsd(
|
||||||
|
LocationSimulationState.adapter,
|
||||||
|
LocationSimulationState.handshake,
|
||||||
|
&LocationSimulationState.remoteServer
|
||||||
|
)
|
||||||
|
if let remoteServerError {
|
||||||
|
idevice_error_free(remoteServerError)
|
||||||
|
LocationSimulationState.cleanup()
|
||||||
|
return LocationSimulationStatus.remoteServer
|
||||||
|
}
|
||||||
|
|
||||||
|
let locationSimulationError = location_simulation_new(
|
||||||
|
LocationSimulationState.remoteServer,
|
||||||
|
&LocationSimulationState.locationSimulation
|
||||||
|
)
|
||||||
|
if let locationSimulationError {
|
||||||
|
idevice_error_free(locationSimulationError)
|
||||||
|
LocationSimulationState.cleanup()
|
||||||
|
return LocationSimulationStatus.locationSimulation
|
||||||
|
}
|
||||||
|
|
||||||
|
LocationSimulationState.remoteServer = nil
|
||||||
|
|
||||||
|
let locationSetError = location_simulation_set(
|
||||||
|
LocationSimulationState.locationSimulation,
|
||||||
|
latitude,
|
||||||
|
longitude
|
||||||
|
)
|
||||||
|
if let locationSetError {
|
||||||
|
idevice_error_free(locationSetError)
|
||||||
|
LocationSimulationState.cleanup()
|
||||||
|
return LocationSimulationStatus.locationSet
|
||||||
|
}
|
||||||
|
|
||||||
|
return LocationSimulationStatus.ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func clear_simulated_location() -> Int32 {
|
||||||
|
guard let locationSimulation = LocationSimulationState.locationSimulation else {
|
||||||
|
return LocationSimulationStatus.locationClear
|
||||||
|
}
|
||||||
|
|
||||||
|
let ffiError = location_simulation_clear(locationSimulation)
|
||||||
|
LocationSimulationState.cleanup()
|
||||||
|
|
||||||
|
if let ffiError {
|
||||||
|
idevice_error_free(ffiError)
|
||||||
|
return LocationSimulationStatus.locationClear
|
||||||
|
}
|
||||||
|
|
||||||
|
return LocationSimulationStatus.ok
|
||||||
|
}
|
||||||
|
|
@ -73,18 +73,14 @@ struct RunningProcessEntity: AppEntity {
|
||||||
/// Resolve the current PID for this process by re-fetching the process list.
|
/// Resolve the current PID for this process by re-fetching the process list.
|
||||||
func resolveCurrentPID() -> Int? {
|
func resolveCurrentPID() -> Int? {
|
||||||
var err: NSError?
|
var err: NSError?
|
||||||
let entries = FetchDeviceProcessList(&err) ?? []
|
let entries = ProcessInfoEntry.currentEntries(&err)
|
||||||
for item in entries {
|
for item in entries {
|
||||||
guard let dict = item as? NSDictionary,
|
|
||||||
let pidNum = dict["pid"] as? NSNumber else { continue }
|
|
||||||
let name = dict["name"] as? String ?? ""
|
|
||||||
let bID = dict["bundleID"] as? String ?? ""
|
|
||||||
// Match by bundle ID first (most stable), then by name
|
// Match by bundle ID first (most stable), then by name
|
||||||
if let myBundle = bundleID, !myBundle.isEmpty, bID == myBundle {
|
if let myBundle = bundleID, !myBundle.isEmpty, item.bundleID == myBundle {
|
||||||
return pidNum.intValue
|
return item.pid
|
||||||
}
|
}
|
||||||
if name == displayName {
|
if item.displayName == displayName {
|
||||||
return pidNum.intValue
|
return item.pid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -118,32 +114,16 @@ struct RunningProcessQuery: EntityStringQuery {
|
||||||
|
|
||||||
private func fetchProcessEntities() throws -> [RunningProcessEntity] {
|
private func fetchProcessEntities() throws -> [RunningProcessEntity] {
|
||||||
var err: NSError?
|
var err: NSError?
|
||||||
let entries = FetchDeviceProcessList(&err) ?? []
|
let entries = ProcessInfoEntry.currentEntries(&err)
|
||||||
if let err { throw err }
|
if let err { throw err }
|
||||||
|
|
||||||
return entries.compactMap { item -> RunningProcessEntity? in
|
return entries.map { entry in
|
||||||
guard let dict = item as? NSDictionary,
|
RunningProcessEntity(
|
||||||
let pidNumber = dict["pid"] as? NSNumber else { return nil }
|
id: entry.stableIdentifier,
|
||||||
let pid = pidNumber.intValue
|
pid: entry.pid,
|
||||||
let name = dict["name"] as? String
|
displayName: entry.displayName,
|
||||||
let bundleID = dict["bundleID"] as? String
|
bundleID: entry.bundleID
|
||||||
let path = dict["path"] as? String ?? ""
|
)
|
||||||
|
|
||||||
let displayName: String
|
|
||||||
if let name, !name.isEmpty {
|
|
||||||
displayName = name
|
|
||||||
} else if let bundleID, !bundleID.isEmpty {
|
|
||||||
displayName = bundleID
|
|
||||||
} else if let last = path.replacingOccurrences(of: "file://", with: "").split(separator: "/").last {
|
|
||||||
displayName = String(last)
|
|
||||||
} else {
|
|
||||||
displayName = "Process \(pid)"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use bundleID as stable ID if available, otherwise fall back to name
|
|
||||||
let stableID = (bundleID != nil && !bundleID!.isEmpty) ? bundleID! : displayName
|
|
||||||
|
|
||||||
return RunningProcessEntity(id: stableID, pid: pid, displayName: displayName, bundleID: bundleID)
|
|
||||||
}
|
}
|
||||||
.sorted { $0.displayName.localizedCaseInsensitiveCompare($1.displayName) == .orderedAscending }
|
.sorted { $0.displayName.localizedCaseInsensitiveCompare($1.displayName) == .orderedAscending }
|
||||||
}
|
}
|
||||||
|
|
@ -176,7 +156,7 @@ struct EnableJITIntent: AppIntent, ForegroundContinuableIntent {
|
||||||
|
|
||||||
var scriptData: Data? = nil
|
var scriptData: Data? = nil
|
||||||
var scriptName: String? = nil
|
var scriptName: String? = nil
|
||||||
if let preferred = IntentScriptResolver.preferredScript(for: bundleID) {
|
if let preferred = ScriptStore.preferredScript(for: bundleID) {
|
||||||
scriptData = preferred.data
|
scriptData = preferred.data
|
||||||
scriptName = preferred.name
|
scriptName = preferred.name
|
||||||
}
|
}
|
||||||
|
|
@ -198,9 +178,10 @@ struct EnableJITIntent: AppIntent, ForegroundContinuableIntent {
|
||||||
userInfo: ["model": model, "scriptData": sd, "scriptName": name]
|
userInfo: ["model": model, "scriptData": sd, "scriptName": name]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
DispatchQueue.global(qos: .background).async {
|
do { try model.runScript(data: sd, name: name) }
|
||||||
do { try model.runScript(data: sd, name: name) }
|
catch {
|
||||||
catch { LogManager.shared.addErrorLog("Script error: \(error.localizedDescription)") }
|
semaphore.signal()
|
||||||
|
LogManager.shared.addErrorLog("Script error: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -322,75 +303,3 @@ func ensureTunnel() async {
|
||||||
}
|
}
|
||||||
try? await Task.sleep(nanoseconds: 1_000_000_000)
|
try? await Task.sleep(nanoseconds: 1_000_000_000)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Script Resolution (mirrors HomeView logic)
|
|
||||||
|
|
||||||
enum IntentScriptResolver {
|
|
||||||
static func preferredScript(for bundleID: String) -> (data: Data, name: String)? {
|
|
||||||
if let assigned = assignedScript(for: bundleID) {
|
|
||||||
return assigned
|
|
||||||
}
|
|
||||||
return autoScript(for: bundleID)
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func assignedScript(for bundleID: String) -> (data: Data, name: String)? {
|
|
||||||
guard let mapping = UserDefaults.standard.dictionary(forKey: "BundleScriptMap") as? [String: String],
|
|
||||||
let scriptName = mapping[bundleID] else { return nil }
|
|
||||||
let scriptsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
|
||||||
.appendingPathComponent("scripts")
|
|
||||||
let scriptURL = scriptsDir.appendingPathComponent(scriptName)
|
|
||||||
guard FileManager.default.fileExists(atPath: scriptURL.path),
|
|
||||||
let data = try? Data(contentsOf: scriptURL) else { return nil }
|
|
||||||
return (data, scriptName)
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func autoScript(for bundleID: String) -> (data: Data, name: String)? {
|
|
||||||
guard ProcessInfo.processInfo.hasTXM else { return nil }
|
|
||||||
guard #available(iOS 26, *) else { return nil }
|
|
||||||
let appName = (try? JITEnableContext.shared.getAppList()[bundleID]) ?? storedFavoriteName(for: bundleID)
|
|
||||||
guard let appName,
|
|
||||||
let resource = autoScriptResource(for: appName) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
let scriptsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
|
||||||
.appendingPathComponent("scripts")
|
|
||||||
let documentsURL = scriptsDir.appendingPathComponent(resource.fileName)
|
|
||||||
if let data = try? Data(contentsOf: documentsURL) {
|
|
||||||
return (data, resource.fileName)
|
|
||||||
}
|
|
||||||
guard let bundleURL = Bundle.main.url(forResource: resource.resource, withExtension: "js"),
|
|
||||||
let data = try? Data(contentsOf: bundleURL) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return (data, resource.fileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func storedFavoriteName(for bundleID: String) -> String? {
|
|
||||||
let defaults = UserDefaults(suiteName: "group.com.stik.sj")
|
|
||||||
let names = defaults?.dictionary(forKey: "favoriteAppNames") as? [String: String]
|
|
||||||
return names?[bundleID]
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func autoScriptResource(for appName: String) -> (resource: String, fileName: String)? {
|
|
||||||
switch appName {
|
|
||||||
case "maciOS":
|
|
||||||
return ("maciOS", "maciOS.js")
|
|
||||||
case "Amethyst", "MeloNX":
|
|
||||||
return ("Amethyst-MeloNX", "Amethyst-MeloNX.js")
|
|
||||||
case "Geode":
|
|
||||||
return ("Geode", "Geode.js")
|
|
||||||
case "Manic EMU":
|
|
||||||
return ("manic", "manic.js")
|
|
||||||
case "UTM", "DolphiniOS", "Flycast":
|
|
||||||
return ("UTM-Dolphin", "UTM-Dolphin.js")
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Notification for JS Script UI
|
|
||||||
|
|
||||||
extension Notification.Name {
|
|
||||||
static let intentJSScriptReady = Notification.Name("intentJSScriptReady")
|
|
||||||
}
|
|
||||||
|
|
|
||||||
771
StikJIT/Utilities/JITEnableContext.swift
Normal file
771
StikJIT/Utilities/JITEnableContext.swift
Normal file
|
|
@ -0,0 +1,771 @@
|
||||||
|
//
|
||||||
|
// JITEnableContext.swift
|
||||||
|
// StikDebug
|
||||||
|
//
|
||||||
|
// Created by Stephen on 2026/3/30.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import idevice
|
||||||
|
import Darwin
|
||||||
|
|
||||||
|
typealias LogFunc = (String?) -> Void
|
||||||
|
typealias DebugAppCallback = (_ pid: Int32, _ debugProxy: OpaquePointer?, _ remoteServer: OpaquePointer?, _ semaphore: DispatchSemaphore) -> Void
|
||||||
|
typealias SyslogLineHandler = (String) -> Void
|
||||||
|
typealias SyslogErrorHandler = (NSError?) -> Void
|
||||||
|
|
||||||
|
final class JITEnableContext {
|
||||||
|
static let shared = JITEnableContext()
|
||||||
|
|
||||||
|
private struct TunnelHandles {
|
||||||
|
var adapter: OpaquePointer?
|
||||||
|
var handshake: OpaquePointer?
|
||||||
|
|
||||||
|
mutating func free() {
|
||||||
|
if let handshake {
|
||||||
|
rsd_handshake_free(handshake)
|
||||||
|
self.handshake = nil
|
||||||
|
}
|
||||||
|
if let adapter {
|
||||||
|
adapter_free(adapter)
|
||||||
|
self.adapter = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var adapter: OpaquePointer?
|
||||||
|
private var handshake: OpaquePointer?
|
||||||
|
|
||||||
|
private let tunnelLock = NSLock()
|
||||||
|
private var tunnelConnecting = false
|
||||||
|
private var tunnelSemaphore: DispatchSemaphore?
|
||||||
|
private var lastTunnelError: NSError?
|
||||||
|
|
||||||
|
private let syslogQueue = DispatchQueue(label: "com.stik.syslogrelay.queue")
|
||||||
|
private var syslogStreaming = false
|
||||||
|
private var syslogClient: OpaquePointer?
|
||||||
|
private var syslogLineHandler: SyslogLineHandler?
|
||||||
|
private var syslogErrorHandler: SyslogErrorHandler?
|
||||||
|
|
||||||
|
var adapterHandle: OpaquePointer? { adapter }
|
||||||
|
var handshakeHandle: OpaquePointer? { handshake }
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
let logURL = FileManager.default
|
||||||
|
.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||||
|
.appendingPathComponent("idevice_log.txt")
|
||||||
|
|
||||||
|
var path = Array(logURL.path.utf8CString)
|
||||||
|
path.withUnsafeMutableBufferPointer { buffer in
|
||||||
|
_ = idevice_init_logger(Info, Debug, buffer.baseAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
stopSyslogRelay()
|
||||||
|
if let handshake {
|
||||||
|
rsd_handshake_free(handshake)
|
||||||
|
}
|
||||||
|
if let adapter {
|
||||||
|
adapter_free(adapter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeError(_ message: String, code: Int = -1) -> NSError {
|
||||||
|
NSError(
|
||||||
|
domain: "StikJIT",
|
||||||
|
code: code,
|
||||||
|
userInfo: [NSLocalizedDescriptionKey: message]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func nsString(from cString: UnsafePointer<CChar>?, fallback: String) -> String {
|
||||||
|
guard let cString, let string = String(validatingUTF8: cString) else {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return string
|
||||||
|
}
|
||||||
|
|
||||||
|
private func error(from ffiError: UnsafeMutablePointer<IdeviceFfiError>?, fallback: String) -> NSError {
|
||||||
|
guard let ffiError else {
|
||||||
|
return makeError(fallback)
|
||||||
|
}
|
||||||
|
let message = nsString(from: ffiError.pointee.message, fallback: fallback)
|
||||||
|
let error = makeError(message, code: Int(ffiError.pointee.code))
|
||||||
|
idevice_error_free(ffiError)
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
|
private func routeLog(_ message: String) {
|
||||||
|
if message.localizedCaseInsensitiveContains("error") {
|
||||||
|
LogManager.shared.addErrorLog(message)
|
||||||
|
} else if message.localizedCaseInsensitiveContains("warning") {
|
||||||
|
LogManager.shared.addWarningLog(message)
|
||||||
|
} else if message.localizedCaseInsensitiveContains("debug") {
|
||||||
|
LogManager.shared.addDebugLog(message)
|
||||||
|
} else {
|
||||||
|
LogManager.shared.addInfoLog(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func emitLog(_ message: String, logger: LogFunc?) {
|
||||||
|
routeLog(message)
|
||||||
|
logger?(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getPairingFile() throws -> OpaquePointer {
|
||||||
|
let pairingFileURL = PairingFileStore.prepareURL()
|
||||||
|
|
||||||
|
guard FileManager.default.fileExists(atPath: pairingFileURL.path) else {
|
||||||
|
throw makeError("Pairing file not found!", code: -17)
|
||||||
|
}
|
||||||
|
|
||||||
|
var pairingFile: OpaquePointer?
|
||||||
|
let ffiError = pairingFileURL.path.withCString { path in
|
||||||
|
rp_pairing_file_read(path, &pairingFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let ffiError {
|
||||||
|
throw error(from: ffiError, fallback: "Failed to read pairing file!")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let pairingFile else {
|
||||||
|
throw makeError("Failed to read pairing file!", code: -17)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pairingFile
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createTunnel(hostname: String) throws -> TunnelHandles {
|
||||||
|
let pairingFile = try getPairingFile()
|
||||||
|
defer { rp_pairing_file_free(pairingFile) }
|
||||||
|
|
||||||
|
var addr = sockaddr_in()
|
||||||
|
addr.sin_family = sa_family_t(AF_INET)
|
||||||
|
addr.sin_port = in_port_t(49152).bigEndian
|
||||||
|
|
||||||
|
let deviceIP = DeviceConnectionContext.targetIPAddress
|
||||||
|
let parseResult = deviceIP.withCString { inet_pton(AF_INET, $0, &addr.sin_addr) }
|
||||||
|
guard parseResult == 1 else {
|
||||||
|
throw makeError("Failed to parse target IP address.", code: -18)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tunnel = TunnelHandles()
|
||||||
|
let ffiError = hostname.withCString { hostname in
|
||||||
|
withUnsafePointer(to: &addr) { pointer in
|
||||||
|
pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) {
|
||||||
|
tunnel_create_rppairing(
|
||||||
|
$0,
|
||||||
|
socklen_t(MemoryLayout<sockaddr_in>.stride),
|
||||||
|
hostname,
|
||||||
|
pairingFile,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
&tunnel.adapter,
|
||||||
|
&tunnel.handshake
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let ffiError {
|
||||||
|
throw error(from: ffiError, fallback: "Failed to create tunnel")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard tunnel.adapter != nil, tunnel.handshake != nil else {
|
||||||
|
var incompleteTunnel = tunnel
|
||||||
|
incompleteTunnel.free()
|
||||||
|
throw makeError("Tunnel was created without valid handles")
|
||||||
|
}
|
||||||
|
|
||||||
|
return tunnel
|
||||||
|
}
|
||||||
|
|
||||||
|
func startTunnel() throws {
|
||||||
|
tunnelLock.lock()
|
||||||
|
if tunnelConnecting {
|
||||||
|
let waitSemaphore = tunnelSemaphore
|
||||||
|
tunnelLock.unlock()
|
||||||
|
|
||||||
|
if let waitSemaphore {
|
||||||
|
waitSemaphore.wait()
|
||||||
|
waitSemaphore.signal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if let lastTunnelError {
|
||||||
|
throw lastTunnelError
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnelConnecting = true
|
||||||
|
let completionSemaphore = DispatchSemaphore(value: 0)
|
||||||
|
tunnelSemaphore = completionSemaphore
|
||||||
|
tunnelLock.unlock()
|
||||||
|
|
||||||
|
var newAdapter: OpaquePointer?
|
||||||
|
var newHandshake: OpaquePointer?
|
||||||
|
var finalError: NSError?
|
||||||
|
|
||||||
|
defer {
|
||||||
|
tunnelLock.lock()
|
||||||
|
tunnelConnecting = false
|
||||||
|
tunnelSemaphore = nil
|
||||||
|
lastTunnelError = finalError
|
||||||
|
tunnelLock.unlock()
|
||||||
|
completionSemaphore.signal()
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let newTunnel = try createTunnel(hostname: "StikDebug")
|
||||||
|
newAdapter = newTunnel.adapter
|
||||||
|
newHandshake = newTunnel.handshake
|
||||||
|
} catch let tunnelError as NSError {
|
||||||
|
finalError = tunnelError
|
||||||
|
throw tunnelError
|
||||||
|
}
|
||||||
|
|
||||||
|
if let handshake {
|
||||||
|
rsd_handshake_free(handshake)
|
||||||
|
}
|
||||||
|
if let adapter {
|
||||||
|
adapter_free(adapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter = newAdapter
|
||||||
|
handshake = newHandshake
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureTunnel() throws {
|
||||||
|
if adapter == nil || handshake == nil {
|
||||||
|
try startTunnel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func withFreshDebugTunnel<T>(
|
||||||
|
hostname: String,
|
||||||
|
_ body: (OpaquePointer, OpaquePointer) throws -> T
|
||||||
|
) throws -> T {
|
||||||
|
var tunnel = try createTunnel(hostname: hostname)
|
||||||
|
defer { tunnel.free() }
|
||||||
|
|
||||||
|
guard let adapter = tunnel.adapter, let handshake = tunnel.handshake else {
|
||||||
|
throw makeError("Tunnel is not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
return try body(adapter, handshake)
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct DebugSession {
|
||||||
|
var remoteServer: OpaquePointer?
|
||||||
|
var debugProxy: OpaquePointer?
|
||||||
|
|
||||||
|
mutating func free() {
|
||||||
|
if let debugProxy {
|
||||||
|
debug_proxy_free(debugProxy)
|
||||||
|
self.debugProxy = nil
|
||||||
|
}
|
||||||
|
if let remoteServer {
|
||||||
|
remote_server_free(remoteServer)
|
||||||
|
self.remoteServer = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class DebugHeartbeatKeepAlive {
|
||||||
|
private static let defaultInterval: UInt64 = 2
|
||||||
|
private static let maxInterval: UInt64 = 3
|
||||||
|
|
||||||
|
private let queue = DispatchQueue(label: "com.stikdebug.debug-heartbeat", qos: .utility)
|
||||||
|
private let stateLock = NSLock()
|
||||||
|
private let startupSemaphore = DispatchSemaphore(value: 0)
|
||||||
|
private let stoppedSemaphore = DispatchSemaphore(value: 0)
|
||||||
|
private let logger: LogFunc?
|
||||||
|
private let makeClient: () throws -> (client: OpaquePointer, tunnel: TunnelHandles)
|
||||||
|
private let errorBuilder: (UnsafeMutablePointer<IdeviceFfiError>?, String) -> NSError
|
||||||
|
private var startupError: NSError?
|
||||||
|
private var client: OpaquePointer?
|
||||||
|
private var tunnel: TunnelHandles?
|
||||||
|
private var stopRequested = false
|
||||||
|
|
||||||
|
init(
|
||||||
|
logger: LogFunc?,
|
||||||
|
makeClient: @escaping () throws -> (client: OpaquePointer, tunnel: TunnelHandles),
|
||||||
|
errorBuilder: @escaping (UnsafeMutablePointer<IdeviceFfiError>?, String) -> NSError
|
||||||
|
) {
|
||||||
|
self.logger = logger
|
||||||
|
self.makeClient = makeClient
|
||||||
|
self.errorBuilder = errorBuilder
|
||||||
|
}
|
||||||
|
|
||||||
|
func start() throws {
|
||||||
|
startupError = nil
|
||||||
|
queue.async { [weak self] in
|
||||||
|
self?.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
startupSemaphore.wait()
|
||||||
|
if let startupError {
|
||||||
|
throw startupError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stop() {
|
||||||
|
stateLock.lock()
|
||||||
|
stopRequested = true
|
||||||
|
stateLock.unlock()
|
||||||
|
_ = stoppedSemaphore.wait(timeout: .now() + .seconds(Int(Self.maxInterval + 1)))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func shouldStop() -> Bool {
|
||||||
|
stateLock.lock()
|
||||||
|
defer { stateLock.unlock() }
|
||||||
|
return stopRequested
|
||||||
|
}
|
||||||
|
|
||||||
|
private func log(_ message: String) {
|
||||||
|
logger?(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func run() {
|
||||||
|
do {
|
||||||
|
let resources = try makeClient()
|
||||||
|
client = resources.client
|
||||||
|
tunnel = resources.tunnel
|
||||||
|
startupError = nil
|
||||||
|
} catch let error as NSError {
|
||||||
|
startupError = error
|
||||||
|
startupSemaphore.signal()
|
||||||
|
stoppedSemaphore.signal()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer {
|
||||||
|
if let client {
|
||||||
|
heartbeat_client_free(client)
|
||||||
|
self.client = nil
|
||||||
|
}
|
||||||
|
if var tunnel = tunnel {
|
||||||
|
tunnel.free()
|
||||||
|
self.tunnel = nil
|
||||||
|
}
|
||||||
|
stoppedSemaphore.signal()
|
||||||
|
}
|
||||||
|
|
||||||
|
startupSemaphore.signal()
|
||||||
|
|
||||||
|
var interval = Self.defaultInterval
|
||||||
|
|
||||||
|
while !shouldStop(), let client {
|
||||||
|
var suggestedInterval: UInt64 = 0
|
||||||
|
let ffiError = heartbeat_get_marco(client, interval, &suggestedInterval)
|
||||||
|
|
||||||
|
if shouldStop() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if let ffiError {
|
||||||
|
let heartbeatError = errorBuilder(ffiError, "Debug heartbeat failed")
|
||||||
|
let description = heartbeatError.localizedDescription
|
||||||
|
|
||||||
|
if description.contains("HeartbeatTimeout") {
|
||||||
|
interval = Self.defaultInterval
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if description.contains("HeartbeatSleepyTime") {
|
||||||
|
log("Debug heartbeat stopped: device entered SleepyTime")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
log("Debug heartbeat warning: \(description)")
|
||||||
|
interval = Self.defaultInterval
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
interval = min(max(suggestedInterval, 1), Self.maxInterval)
|
||||||
|
|
||||||
|
if let ffiError = heartbeat_send_polo(client) {
|
||||||
|
let heartbeatError = errorBuilder(ffiError, "Failed to reply to heartbeat")
|
||||||
|
log("Debug heartbeat warning: \(heartbeatError.localizedDescription)")
|
||||||
|
interval = Self.defaultInterval
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func connectDebugSession(adapter: OpaquePointer, handshake: OpaquePointer) throws -> DebugSession {
|
||||||
|
var session = DebugSession()
|
||||||
|
|
||||||
|
if let ffiError = remote_server_connect_rsd(adapter, handshake, &session.remoteServer) {
|
||||||
|
throw error(from: ffiError, fallback: "Failed to connect remote server")
|
||||||
|
}
|
||||||
|
|
||||||
|
if let ffiError = debug_proxy_connect_rsd(adapter, handshake, &session.debugProxy) {
|
||||||
|
session.free()
|
||||||
|
throw error(from: ffiError, fallback: "Failed to connect debug proxy")
|
||||||
|
}
|
||||||
|
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
|
private func withConnectedDebugSession<T>(
|
||||||
|
_ body: (OpaquePointer, OpaquePointer) throws -> T
|
||||||
|
) throws -> T {
|
||||||
|
try withFreshDebugTunnel(hostname: "StikDebugDebug") { adapter, handshake in
|
||||||
|
var session = try connectDebugSession(adapter: adapter, handshake: handshake)
|
||||||
|
defer { session.free() }
|
||||||
|
|
||||||
|
guard let remoteServer = session.remoteServer,
|
||||||
|
let debugProxy = session.debugProxy else {
|
||||||
|
throw makeError("Debug session was not created")
|
||||||
|
}
|
||||||
|
|
||||||
|
return try body(remoteServer, debugProxy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func withConnectedRemoteServer<T>(
|
||||||
|
_ body: (OpaquePointer) throws -> T
|
||||||
|
) throws -> T {
|
||||||
|
try ensureTunnel()
|
||||||
|
guard let adapter, let handshake else {
|
||||||
|
throw makeError("Tunnel is not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
var remoteServer: OpaquePointer?
|
||||||
|
if let ffiError = remote_server_connect_rsd(adapter, handshake, &remoteServer) {
|
||||||
|
throw error(from: ffiError, fallback: "Failed to connect remote server")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let remoteServer else {
|
||||||
|
throw makeError("Remote server handle was not created")
|
||||||
|
}
|
||||||
|
|
||||||
|
defer { remote_server_free(remoteServer) }
|
||||||
|
return try body(remoteServer)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func withProcessControl<T>(
|
||||||
|
remoteServer: OpaquePointer,
|
||||||
|
_ body: (OpaquePointer) throws -> T
|
||||||
|
) throws -> T {
|
||||||
|
var processControl: OpaquePointer?
|
||||||
|
if let ffiError = process_control_new(remoteServer, &processControl) {
|
||||||
|
throw error(from: ffiError, fallback: "Failed to open process control")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let processControl else {
|
||||||
|
throw makeError("Process control handle was not created")
|
||||||
|
}
|
||||||
|
|
||||||
|
defer { process_control_free(processControl) }
|
||||||
|
return try body(processControl)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func connectHeartbeatKeepAlive(logger: LogFunc?) throws -> DebugHeartbeatKeepAlive {
|
||||||
|
return DebugHeartbeatKeepAlive(
|
||||||
|
logger: logger,
|
||||||
|
makeClient: { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
throw NSError(
|
||||||
|
domain: "StikJIT",
|
||||||
|
code: -1,
|
||||||
|
userInfo: [NSLocalizedDescriptionKey: "Debug heartbeat context is unavailable"]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tunnel = try self.createTunnel(hostname: "StikDebugHeartbeat")
|
||||||
|
guard let adapter = tunnel.adapter, let handshake = tunnel.handshake else {
|
||||||
|
tunnel.free()
|
||||||
|
throw self.makeError("Tunnel is not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
var heartbeatClient: OpaquePointer?
|
||||||
|
if let ffiError = heartbeat_connect_rsd(adapter, handshake, &heartbeatClient) {
|
||||||
|
tunnel.free()
|
||||||
|
throw self.error(from: ffiError, fallback: "Failed to connect debug heartbeat")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let heartbeatClient else {
|
||||||
|
tunnel.free()
|
||||||
|
throw self.makeError("Heartbeat client was not created")
|
||||||
|
}
|
||||||
|
|
||||||
|
return (client: heartbeatClient, tunnel: tunnel)
|
||||||
|
},
|
||||||
|
errorBuilder: { [weak self] ffiError, fallback in
|
||||||
|
self?.error(from: ffiError, fallback: fallback) ?? NSError(
|
||||||
|
domain: "StikJIT",
|
||||||
|
code: -1,
|
||||||
|
userInfo: [NSLocalizedDescriptionKey: fallback]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sendDebugCommand(_ command: String, debugProxy: OpaquePointer) throws -> String? {
|
||||||
|
guard let commandHandle = debugserver_command_new(command, nil, 0) else {
|
||||||
|
throw makeError("Failed to create debugserver command: \(command)")
|
||||||
|
}
|
||||||
|
|
||||||
|
var response: UnsafeMutablePointer<CChar>?
|
||||||
|
let ffiError = debug_proxy_send_command(debugProxy, commandHandle, &response)
|
||||||
|
debugserver_command_free(commandHandle)
|
||||||
|
|
||||||
|
if let ffiError {
|
||||||
|
if let response {
|
||||||
|
idevice_string_free(response)
|
||||||
|
}
|
||||||
|
throw error(from: ffiError, fallback: "Debugserver command failed: \(command)")
|
||||||
|
}
|
||||||
|
|
||||||
|
defer {
|
||||||
|
if let response {
|
||||||
|
idevice_string_free(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let response else { return nil }
|
||||||
|
return String(cString: response)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func runDebugServerCommand(
|
||||||
|
pid: Int32,
|
||||||
|
debugProxy: OpaquePointer,
|
||||||
|
remoteServer: OpaquePointer,
|
||||||
|
logger: LogFunc?,
|
||||||
|
callback: DebugAppCallback?
|
||||||
|
) {
|
||||||
|
debug_proxy_send_ack(debugProxy)
|
||||||
|
debug_proxy_send_ack(debugProxy)
|
||||||
|
|
||||||
|
do {
|
||||||
|
let response = try sendDebugCommand("QStartNoAckMode", debugProxy: debugProxy) ?? "<nil>"
|
||||||
|
emitLog("QStartNoAckMode result = \(response)", logger: logger)
|
||||||
|
} catch {
|
||||||
|
emitLog(error.localizedDescription, logger: logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
debug_proxy_set_ack_mode(debugProxy, 0)
|
||||||
|
|
||||||
|
if let callback {
|
||||||
|
let keepAlive: DebugHeartbeatKeepAlive?
|
||||||
|
do {
|
||||||
|
let heartbeat = try connectHeartbeatKeepAlive(logger: logger)
|
||||||
|
try heartbeat.start()
|
||||||
|
keepAlive = heartbeat
|
||||||
|
emitLog("Debug heartbeat keepalive started", logger: logger)
|
||||||
|
} catch {
|
||||||
|
keepAlive = nil
|
||||||
|
emitLog("Warning: failed to start debug heartbeat keepalive: \(error.localizedDescription)", logger: logger)
|
||||||
|
}
|
||||||
|
defer {
|
||||||
|
keepAlive?.stop()
|
||||||
|
if keepAlive != nil {
|
||||||
|
emitLog("Debug heartbeat keepalive stopped", logger: logger)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let semaphore = DispatchSemaphore(value: 0)
|
||||||
|
callback(pid, debugProxy, remoteServer, semaphore)
|
||||||
|
semaphore.wait()
|
||||||
|
|
||||||
|
var breakByte: UInt8 = 0x03
|
||||||
|
if let ffiError = debug_proxy_send_raw(debugProxy, &breakByte, 1) {
|
||||||
|
emitLog(error(from: ffiError, fallback: "Failed to interrupt target").localizedDescription, logger: logger)
|
||||||
|
}
|
||||||
|
usleep(500)
|
||||||
|
} else {
|
||||||
|
let attachCommand = "vAttach;\(String(UInt32(pid), radix: 16))"
|
||||||
|
do {
|
||||||
|
let response = try sendDebugCommand(attachCommand, debugProxy: debugProxy) ?? "<nil>"
|
||||||
|
emitLog("Attach response: \(response)", logger: logger)
|
||||||
|
} catch {
|
||||||
|
emitLog(error.localizedDescription, logger: logger)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let response = try sendDebugCommand("D", debugProxy: debugProxy)
|
||||||
|
if let response {
|
||||||
|
emitLog("Detach response: \(response)", logger: logger)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
emitLog(error.localizedDescription, logger: logger)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func debugApp(withBundleID bundleID: String, logger: LogFunc?, jsCallback: DebugAppCallback?) -> Bool {
|
||||||
|
runDebugSession(logger: logger, callback: jsCallback) { remoteServer in
|
||||||
|
try withProcessControl(remoteServer: remoteServer) { processControl in
|
||||||
|
var pid: UInt64 = 0
|
||||||
|
let ffiError = bundleID.withCString { bundleID in
|
||||||
|
process_control_launch_app(processControl, bundleID, nil, 0, nil, 0, true, false, &pid)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let ffiError {
|
||||||
|
throw error(from: ffiError, fallback: "Failed to launch app")
|
||||||
|
}
|
||||||
|
|
||||||
|
return Int32(pid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func debugApp(withPID pid: Int32, logger: LogFunc?, jsCallback: DebugAppCallback?) -> Bool {
|
||||||
|
runDebugSession(logger: logger, callback: jsCallback) { _ in pid }
|
||||||
|
}
|
||||||
|
|
||||||
|
func launchAppWithoutDebug(_ bundleID: String, logger: LogFunc?) -> Bool {
|
||||||
|
do {
|
||||||
|
let pid = try withConnectedRemoteServer { remoteServer in
|
||||||
|
try withProcessControl(remoteServer: remoteServer) { processControl in
|
||||||
|
var pid: UInt64 = 0
|
||||||
|
let ffiError = bundleID.withCString { bundleID in
|
||||||
|
process_control_launch_app(processControl, bundleID, nil, 0, nil, 0, false, true, &pid)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let ffiError {
|
||||||
|
throw error(from: ffiError, fallback: "Failed to launch app")
|
||||||
|
}
|
||||||
|
|
||||||
|
return pid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emitLog("Launched app (PID \(pid))", logger: logger)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
emitLog(error.localizedDescription, logger: logger)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startSyslogRelay(handler: @escaping SyslogLineHandler, onError: @escaping SyslogErrorHandler) {
|
||||||
|
do {
|
||||||
|
try ensureTunnel()
|
||||||
|
} catch let nsError as NSError {
|
||||||
|
onError(nsError)
|
||||||
|
return
|
||||||
|
} catch {
|
||||||
|
onError(makeError(error.localizedDescription))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard !syslogStreaming else { return }
|
||||||
|
|
||||||
|
syslogStreaming = true
|
||||||
|
syslogLineHandler = handler
|
||||||
|
syslogErrorHandler = onError
|
||||||
|
|
||||||
|
syslogQueue.async { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
guard let adapter = self.adapter, let handshake = self.handshake else {
|
||||||
|
self.handleSyslogFailure(self.makeError("Tunnel is not connected"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var client: OpaquePointer?
|
||||||
|
if let ffiError = syslog_relay_connect_rsd(adapter, handshake, &client) {
|
||||||
|
self.handleSyslogFailure(self.error(from: ffiError, fallback: "Failed to connect to syslog relay"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.syslogClient = client
|
||||||
|
|
||||||
|
while self.syslogStreaming, let client = self.syslogClient {
|
||||||
|
var message: UnsafeMutablePointer<CChar>?
|
||||||
|
let ffiError = syslog_relay_next(client, &message)
|
||||||
|
if let ffiError {
|
||||||
|
if let message {
|
||||||
|
idevice_string_free(message)
|
||||||
|
}
|
||||||
|
self.handleSyslogFailure(self.error(from: ffiError, fallback: "Syslog relay read failed"))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let message else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let line = String(cString: message)
|
||||||
|
idevice_string_free(message)
|
||||||
|
|
||||||
|
if let handler = self.syslogLineHandler {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
handler(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let client = self.syslogClient {
|
||||||
|
syslog_relay_client_free(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.syslogClient = nil
|
||||||
|
self.syslogStreaming = false
|
||||||
|
self.syslogLineHandler = nil
|
||||||
|
self.syslogErrorHandler = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopSyslogRelay() {
|
||||||
|
guard syslogStreaming else { return }
|
||||||
|
|
||||||
|
syslogStreaming = false
|
||||||
|
syslogLineHandler = nil
|
||||||
|
syslogErrorHandler = nil
|
||||||
|
|
||||||
|
syslogQueue.async { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
if let syslogClient = self.syslogClient {
|
||||||
|
syslog_relay_client_free(syslogClient)
|
||||||
|
self.syslogClient = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func runDebugSession(
|
||||||
|
logger: LogFunc?,
|
||||||
|
callback: DebugAppCallback?,
|
||||||
|
pidProvider: (OpaquePointer) throws -> Int32
|
||||||
|
) -> Bool {
|
||||||
|
do {
|
||||||
|
try withConnectedDebugSession { remoteServer, debugProxy in
|
||||||
|
let pid = try pidProvider(remoteServer)
|
||||||
|
runDebugServerCommand(
|
||||||
|
pid: pid,
|
||||||
|
debugProxy: debugProxy,
|
||||||
|
remoteServer: remoteServer,
|
||||||
|
logger: logger,
|
||||||
|
callback: callback
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
emitLog("Debug session completed", logger: logger)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
emitLog(error.localizedDescription, logger: logger)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleSyslogFailure(_ error: NSError) {
|
||||||
|
syslogStreaming = false
|
||||||
|
if let syslogClient {
|
||||||
|
syslog_relay_client_free(syslogClient)
|
||||||
|
self.syslogClient = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let errorHandler = syslogErrorHandler
|
||||||
|
syslogLineHandler = nil
|
||||||
|
syslogErrorHandler = nil
|
||||||
|
|
||||||
|
if let errorHandler {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
errorHandler(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
//
|
|
||||||
// ProcessInspectorBridge.h
|
|
||||||
// StikJIT
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "../idevice/JITEnableContext.h"
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
|
||||||
|
|
||||||
FOUNDATION_EXPORT NSArray<NSDictionary*> * _Nullable FetchDeviceProcessList(NSError **error);
|
|
||||||
FOUNDATION_EXPORT BOOL KillDeviceProcess(int pid, NSError **error);
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
//
|
|
||||||
// ProcessInspectorBridge.m
|
|
||||||
// StikJIT
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "ProcessInspectorBridge.h"
|
|
||||||
|
|
||||||
NSArray<NSDictionary*> * _Nullable FetchDeviceProcessList(NSError **error) {
|
|
||||||
@try {
|
|
||||||
return [[JITEnableContext shared] fetchProcessListWithError:error];
|
|
||||||
} @catch (NSException *exception) {
|
|
||||||
if (error) {
|
|
||||||
NSString *message = [NSString stringWithFormat:@"Process fetch failed: %@", exception.reason ?: @"Unknown error"];
|
|
||||||
*error = [NSError errorWithDomain:@"ProcessInspectorBridge"
|
|
||||||
code:-500
|
|
||||||
userInfo:@{NSLocalizedDescriptionKey: message}];
|
|
||||||
}
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BOOL KillDeviceProcess(int pid, NSError **error) {
|
|
||||||
@try {
|
|
||||||
return [[JITEnableContext shared] killProcessWithPID:pid error:error];
|
|
||||||
} @catch (NSException *exception) {
|
|
||||||
if (error) {
|
|
||||||
NSString *message = [NSString stringWithFormat:@"Process kill failed: %@", exception.reason ?: @"Unknown error"];
|
|
||||||
*error = [NSError errorWithDomain:@"ProcessInspectorBridge"
|
|
||||||
code:-501
|
|
||||||
userInfo:@{NSLocalizedDescriptionKey: message}];
|
|
||||||
}
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
// from MeloNX
|
// from MeloNX
|
||||||
// Created by s s on 2025/4/6.
|
// Created by s s on 2025/4/6.
|
||||||
//
|
//
|
||||||
|
import Foundation
|
||||||
import Security
|
import Security
|
||||||
|
|
||||||
typealias SecTaskRef = OpaquePointer
|
typealias SecTaskRef = OpaquePointer
|
||||||
|
|
@ -11,8 +12,8 @@ typealias SecTaskRef = OpaquePointer
|
||||||
@_silgen_name("SecTaskCopyValueForEntitlement")
|
@_silgen_name("SecTaskCopyValueForEntitlement")
|
||||||
func SecTaskCopyValueForEntitlement(
|
func SecTaskCopyValueForEntitlement(
|
||||||
_ task: SecTaskRef,
|
_ task: SecTaskRef,
|
||||||
_ entitlement: NSString,
|
_ entitlement: CFString,
|
||||||
_ error: NSErrorPointer
|
_ error: UnsafeMutablePointer<Unmanaged<CFError>?>?
|
||||||
) -> CFTypeRef?
|
) -> CFTypeRef?
|
||||||
|
|
||||||
@_silgen_name("SecTaskCreateFromSelf")
|
@_silgen_name("SecTaskCreateFromSelf")
|
||||||
|
|
@ -22,6 +23,6 @@ func SecTaskCreateFromSelf(
|
||||||
|
|
||||||
func checkAppEntitlement(_ ent: String) -> Bool {
|
func checkAppEntitlement(_ ent: String) -> Bool {
|
||||||
guard let task = SecTaskCreateFromSelf(nil) else { return false }
|
guard let task = SecTaskCreateFromSelf(nil) else { return false }
|
||||||
guard let value = SecTaskCopyValueForEntitlement(task, ent as NSString, nil) else { return false }
|
guard let value = SecTaskCopyValueForEntitlement(task, ent as CFString, nil) else { return false }
|
||||||
return value.boolValue != nil && value.boolValue
|
return value.boolValue != nil && value.boolValue
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,10 +55,13 @@ final class SystemLogStream: ObservableObject {
|
||||||
startBatchTimer()
|
startBatchTimer()
|
||||||
|
|
||||||
JITEnableContext.shared.startSyslogRelay(handler: { [weak self] line in
|
JITEnableContext.shared.startSyslogRelay(handler: { [weak self] line in
|
||||||
guard let line else { return }
|
Task { @MainActor [weak self] in
|
||||||
self?.handleLine(line)
|
self?.handleLine(line)
|
||||||
|
}
|
||||||
}, onError: { [weak self] error in
|
}, onError: { [weak self] error in
|
||||||
self?.handleError(error as NSError?)
|
Task { @MainActor [weak self] in
|
||||||
|
self?.handleError(error)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,9 +139,11 @@ final class SystemLogStream: ObservableObject {
|
||||||
private func startBatchTimer() {
|
private func startBatchTimer() {
|
||||||
batchTimer?.invalidate()
|
batchTimer?.invalidate()
|
||||||
batchTimer = Timer.scheduledTimer(withTimeInterval: Self.batchInterval, repeats: true) { [weak self] _ in
|
batchTimer = Timer.scheduledTimer(withTimeInterval: Self.batchInterval, repeats: true) { [weak self] _ in
|
||||||
guard let self else { return }
|
Task { @MainActor [weak self] in
|
||||||
guard !self.isPaused, self.updateInterval == 0, !self.pendingEntries.isEmpty else { return }
|
guard let self else { return }
|
||||||
self.flushAllPending()
|
guard !self.isPaused, self.updateInterval == 0, !self.pendingEntries.isEmpty else { return }
|
||||||
|
self.flushAllPending()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let batchTimer {
|
if let batchTimer {
|
||||||
RunLoop.main.add(batchTimer, forMode: .common)
|
RunLoop.main.add(batchTimer, forMode: .common)
|
||||||
|
|
@ -178,12 +183,14 @@ final class SystemLogStream: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
flushTimer = Timer.scheduledTimer(withTimeInterval: updateInterval, repeats: false) { [weak self] _ in
|
flushTimer = Timer.scheduledTimer(withTimeInterval: updateInterval, repeats: false) { [weak self] _ in
|
||||||
guard let self else { return }
|
Task { @MainActor [weak self] in
|
||||||
self.flushTimer = nil
|
guard let self else { return }
|
||||||
guard !self.isPaused else { return }
|
self.flushTimer = nil
|
||||||
self.flushAllPending()
|
guard !self.isPaused else { return }
|
||||||
if !self.pendingEntries.isEmpty {
|
self.flushAllPending()
|
||||||
self.scheduleFlushIfNeeded()
|
if !self.pendingEntries.isEmpty {
|
||||||
|
self.scheduleFlushIfNeeded()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -196,10 +203,12 @@ final class SystemLogStream: ObservableObject {
|
||||||
guard !isStreaming else { return }
|
guard !isStreaming else { return }
|
||||||
guard retryTimer == nil else { return }
|
guard retryTimer == nil else { return }
|
||||||
let timer = Timer(timeInterval: 2.0, repeats: false) { [weak self] _ in
|
let timer = Timer(timeInterval: 2.0, repeats: false) { [weak self] _ in
|
||||||
guard let self else { return }
|
Task { @MainActor [weak self] in
|
||||||
self.retryTimer = nil
|
guard let self else { return }
|
||||||
if !self.isStreaming && !self.isPaused {
|
self.retryTimer = nil
|
||||||
self.start()
|
if !self.isStreaming && !self.isPaused {
|
||||||
|
self.start()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
retryTimer = timer
|
retryTimer = timer
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ struct ConsoleLogsView: View {
|
||||||
@State private var showingCustomAlert = false
|
@State private var showingCustomAlert = false
|
||||||
@State private var alertMessage = ""
|
@State private var alertMessage = ""
|
||||||
@State private var alertTitle = ""
|
@State private var alertTitle = ""
|
||||||
@State private var isError = false
|
|
||||||
|
|
||||||
@State private var logCheckTimer: Timer? = nil
|
@State private var logCheckTimer: Timer? = nil
|
||||||
|
|
||||||
|
|
@ -111,12 +110,9 @@ struct ConsoleLogsView: View {
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
systemLogStream.stop()
|
systemLogStream.stop()
|
||||||
}
|
}
|
||||||
.onChange(of: systemLogStream.lastError) { newError in
|
.onChange(of: systemLogStream.lastError) { _, newError in
|
||||||
if let error = newError {
|
if let error = newError {
|
||||||
alertTitle = "Syslog Error"
|
presentAlert(title: "Syslog Error", message: error)
|
||||||
alertMessage = error
|
|
||||||
isError = true
|
|
||||||
showingCustomAlert = true
|
|
||||||
systemLogStream.lastError = nil
|
systemLogStream.lastError = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -177,7 +173,7 @@ struct ConsoleLogsView: View {
|
||||||
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { offset in
|
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { offset in
|
||||||
jitIsAtBottom = offset > -20
|
jitIsAtBottom = offset > -20
|
||||||
}
|
}
|
||||||
.onChange(of: logManager.logs.count) { _ in
|
.onChange(of: logManager.logs.count) { _, _ in
|
||||||
guard jitIsAtBottom, let lastLog = logManager.logs.last else { return }
|
guard jitIsAtBottom, let lastLog = logManager.logs.last else { return }
|
||||||
withAnimation {
|
withAnimation {
|
||||||
proxy.scrollTo(lastLog.id, anchor: .bottom)
|
proxy.scrollTo(lastLog.id, anchor: .bottom)
|
||||||
|
|
@ -246,7 +242,7 @@ struct ConsoleLogsView: View {
|
||||||
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { offset in
|
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { offset in
|
||||||
syslogIsAtBottom = offset > -20
|
syslogIsAtBottom = offset > -20
|
||||||
}
|
}
|
||||||
.onChange(of: systemLogStream.entries.count) { _ in
|
.onChange(of: systemLogStream.entries.count) { _, _ in
|
||||||
guard syslogIsAtBottom, syslogSearchText.isEmpty,
|
guard syslogIsAtBottom, syslogSearchText.isEmpty,
|
||||||
let lastLog = systemLogStream.entries.last else { return }
|
let lastLog = systemLogStream.entries.last else { return }
|
||||||
withAnimation {
|
withAnimation {
|
||||||
|
|
@ -276,10 +272,7 @@ struct ConsoleLogsView: View {
|
||||||
"[\(formatTime(date: $0.timestamp))] [\($0.type.rawValue)] \($0.message)"
|
"[\(formatTime(date: $0.timestamp))] [\($0.type.rawValue)] \($0.message)"
|
||||||
}.joined(separator: "\n")
|
}.joined(separator: "\n")
|
||||||
UIPasteboard.general.string = logsContent
|
UIPasteboard.general.string = logsContent
|
||||||
alertTitle = "Logs Copied"
|
presentAlert(title: "Logs Copied", message: "Logs have been copied to clipboard.")
|
||||||
alertMessage = "Logs have been copied to clipboard."
|
|
||||||
isError = false
|
|
||||||
showingCustomAlert = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|
@ -294,10 +287,7 @@ struct ConsoleLogsView: View {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Button("Export Logs", systemImage: "square.and.arrow.up") {
|
Button("Export Logs", systemImage: "square.and.arrow.up") {
|
||||||
alertTitle = "Export Failed"
|
presentAlert(title: "Export Failed", message: "No idevice logs found")
|
||||||
alertMessage = "No idevice logs found"
|
|
||||||
isError = true
|
|
||||||
showingCustomAlert = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -332,7 +322,7 @@ struct ConsoleLogsView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createSyslogAttributedString(_ entry: SystemLogStream.Entry) -> NSAttributedString {
|
private func createSyslogAttributedString(_ entry: SystemLogStream.Entry) -> NSAttributedString {
|
||||||
let type = logType(for: entry.raw)
|
let type = Self.logType(for: entry.raw)
|
||||||
let fullString = NSMutableAttributedString()
|
let fullString = NSMutableAttributedString()
|
||||||
|
|
||||||
let timestampString = "[\(DateFormatter.consoleLogsFormatter.string(from: entry.timestamp))]"
|
let timestampString = "[\(DateFormatter.consoleLogsFormatter.string(from: entry.timestamp))]"
|
||||||
|
|
@ -358,9 +348,7 @@ struct ConsoleLogsView: View {
|
||||||
return fullString
|
return fullString
|
||||||
}
|
}
|
||||||
private func formatTime(date: Date) -> String {
|
private func formatTime(date: Date) -> String {
|
||||||
let formatter = DateFormatter()
|
DateFormatter.consoleLogsFormatter.string(from: date)
|
||||||
formatter.dateFormat = "HH:mm:ss"
|
|
||||||
return formatter.string(from: date)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func colorForLogType(_ type: LogManager.LogEntry.LogType) -> Color {
|
private func colorForLogType(_ type: LogManager.LogEntry.LogType) -> Color {
|
||||||
|
|
@ -376,7 +364,7 @@ struct ConsoleLogsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func logType(for line: String) -> LogManager.LogEntry.LogType {
|
nonisolated private static func logType(for line: String) -> LogManager.LogEntry.LogType {
|
||||||
let lowercase = line.lowercased()
|
let lowercase = line.lowercased()
|
||||||
if lowercase.contains("error") {
|
if lowercase.contains("error") {
|
||||||
return .error
|
return .error
|
||||||
|
|
@ -391,7 +379,7 @@ struct ConsoleLogsView: View {
|
||||||
|
|
||||||
private var syslogErrorCount: Int {
|
private var syslogErrorCount: Int {
|
||||||
systemLogStream.entries.reduce(0) { count, entry in
|
systemLogStream.entries.reduce(0) { count, entry in
|
||||||
count + (logType(for: entry.raw) == .error ? 1 : 0)
|
count + (Self.logType(for: entry.raw) == .error ? 1 : 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -428,26 +416,7 @@ struct ConsoleLogsView: View {
|
||||||
|
|
||||||
let skipPrefixes = ["=== DEVICE INFORMATION ===", "Version:", "Name:", "Model:", "=== LOG ENTRIES ==="]
|
let skipPrefixes = ["=== DEVICE INFORMATION ===", "Version:", "Name:", "Model:", "=== LOG ENTRIES ==="]
|
||||||
|
|
||||||
var parsed: [LogManager.LogEntry] = []
|
let parsed = Self.parseAppLogEntries(from: recentLines, skipPrefixes: skipPrefixes)
|
||||||
parsed.reserveCapacity(recentLines.count)
|
|
||||||
|
|
||||||
for line in recentLines {
|
|
||||||
if line.isEmpty { continue }
|
|
||||||
if skipPrefixes.contains(where: { line.contains($0) }) { continue }
|
|
||||||
|
|
||||||
let type: LogManager.LogEntry.LogType
|
|
||||||
if line.contains("ERROR") || line.contains("Error") {
|
|
||||||
type = .error
|
|
||||||
} else if line.contains("WARNING") || line.contains("Warning") {
|
|
||||||
type = .warning
|
|
||||||
} else if line.contains("DEBUG") {
|
|
||||||
type = .debug
|
|
||||||
} else {
|
|
||||||
type = .info
|
|
||||||
}
|
|
||||||
parsed.append(LogManager.LogEntry(timestamp: Date(), type: type, message: line))
|
|
||||||
}
|
|
||||||
|
|
||||||
return (parsed, lines.count)
|
return (parsed, lines.count)
|
||||||
} catch {
|
} catch {
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -501,25 +470,7 @@ struct ConsoleLogsView: View {
|
||||||
guard lines.count > previousCount else { return ([], lines.count) }
|
guard lines.count > previousCount else { return ([], lines.count) }
|
||||||
|
|
||||||
let newLines = lines[previousCount..<lines.count]
|
let newLines = lines[previousCount..<lines.count]
|
||||||
var parsed: [LogManager.LogEntry] = []
|
let parsed = Self.parseAppLogEntries(from: newLines)
|
||||||
parsed.reserveCapacity(newLines.count)
|
|
||||||
|
|
||||||
for line in newLines {
|
|
||||||
if line.isEmpty { continue }
|
|
||||||
|
|
||||||
let type: LogManager.LogEntry.LogType
|
|
||||||
if line.contains("ERROR") || line.contains("Error") {
|
|
||||||
type = .error
|
|
||||||
} else if line.contains("WARNING") || line.contains("Warning") {
|
|
||||||
type = .warning
|
|
||||||
} else if line.contains("DEBUG") {
|
|
||||||
type = .debug
|
|
||||||
} else {
|
|
||||||
type = .info
|
|
||||||
}
|
|
||||||
parsed.append(LogManager.LogEntry(timestamp: Date(), type: type, message: line))
|
|
||||||
}
|
|
||||||
|
|
||||||
return (parsed, lines.count)
|
return (parsed, lines.count)
|
||||||
} catch {
|
} catch {
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -558,10 +509,8 @@ struct ConsoleLogsView: View {
|
||||||
private func copySyslogToClipboard() {
|
private func copySyslogToClipboard() {
|
||||||
let entries = filteredSyslogEntries
|
let entries = filteredSyslogEntries
|
||||||
guard !entries.isEmpty else {
|
guard !entries.isEmpty else {
|
||||||
alertTitle = "Export Failed"
|
let message = syslogSearchText.isEmpty ? "No syslog entries to copy." : "No matching syslog entries to copy."
|
||||||
alertMessage = syslogSearchText.isEmpty ? "No syslog entries to copy." : "No matching syslog entries to copy."
|
presentAlert(title: "Export Failed", message: message)
|
||||||
isError = true
|
|
||||||
showingCustomAlert = true
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -570,12 +519,10 @@ struct ConsoleLogsView: View {
|
||||||
}.joined(separator: "\n")
|
}.joined(separator: "\n")
|
||||||
|
|
||||||
UIPasteboard.general.string = content
|
UIPasteboard.general.string = content
|
||||||
alertTitle = "Logs Copied"
|
let message = syslogSearchText.isEmpty
|
||||||
alertMessage = syslogSearchText.isEmpty
|
|
||||||
? "Latest syslog entries copied to clipboard."
|
? "Latest syslog entries copied to clipboard."
|
||||||
: "\(entries.count) filtered syslog entries copied to clipboard."
|
: "\(entries.count) filtered syslog entries copied to clipboard."
|
||||||
isError = false
|
presentAlert(title: "Logs Copied", message: message)
|
||||||
showingCustomAlert = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var syslogControlIcon: String {
|
private var syslogControlIcon: String {
|
||||||
|
|
@ -592,6 +539,24 @@ struct ConsoleLogsView: View {
|
||||||
return systemLogStream.isPaused ? "Resume syslog stream" : "Pause syslog stream"
|
return systemLogStream.isPaused ? "Resume syslog stream" : "Pause syslog stream"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func presentAlert(title: String, message: String) {
|
||||||
|
alertTitle = title
|
||||||
|
alertMessage = message
|
||||||
|
showingCustomAlert = true
|
||||||
|
}
|
||||||
|
|
||||||
|
nonisolated private static func parseAppLogEntries<S: Sequence>(from lines: S, skipPrefixes: [String] = []) -> [LogManager.LogEntry] where S.Element == String {
|
||||||
|
var parsed: [LogManager.LogEntry] = []
|
||||||
|
parsed.reserveCapacity(lines.underestimatedCount)
|
||||||
|
|
||||||
|
for line in lines {
|
||||||
|
if line.isEmpty { continue }
|
||||||
|
if skipPrefixes.contains(where: { line.contains($0) }) { continue }
|
||||||
|
parsed.append(LogManager.LogEntry(timestamp: Date(), type: Self.logType(for: line), message: line))
|
||||||
|
}
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UniformTypeIdentifiers
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
struct JITEnableConfiguration {
|
struct JITEnableConfiguration {
|
||||||
|
|
@ -16,6 +15,63 @@ struct JITEnableConfiguration {
|
||||||
var scriptName : String? = nil
|
var scriptName : String? = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class DebugKeepAliveLease {
|
||||||
|
private let stateLock = NSLock()
|
||||||
|
private var isActive = false
|
||||||
|
private var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
|
||||||
|
|
||||||
|
init() {
|
||||||
|
activate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func invalidate() {
|
||||||
|
stateLock.lock()
|
||||||
|
guard isActive else {
|
||||||
|
stateLock.unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isActive = false
|
||||||
|
stateLock.unlock()
|
||||||
|
|
||||||
|
runOnMain {
|
||||||
|
BackgroundAudioManager.shared.requestStop()
|
||||||
|
BackgroundLocationManager.shared.requestStop()
|
||||||
|
|
||||||
|
if self.backgroundTaskID != .invalid {
|
||||||
|
UIApplication.shared.endBackgroundTask(self.backgroundTaskID)
|
||||||
|
self.backgroundTaskID = .invalid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func activate() {
|
||||||
|
stateLock.lock()
|
||||||
|
guard !isActive else {
|
||||||
|
stateLock.unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isActive = true
|
||||||
|
stateLock.unlock()
|
||||||
|
|
||||||
|
runOnMain {
|
||||||
|
BackgroundAudioManager.shared.requestStart()
|
||||||
|
BackgroundLocationManager.shared.requestStart()
|
||||||
|
self.backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "StikDebugDebugSession") { [weak self] in
|
||||||
|
LogManager.shared.addWarningLog("Debug session background task expired")
|
||||||
|
self?.invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func runOnMain(_ work: @escaping () -> Void) {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
work()
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync(execute: work)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct HomeView: View {
|
struct HomeView: View {
|
||||||
|
|
||||||
@AppStorage("autoQuitAfterEnablingJIT") private var doAutoQuitAfterEnablingJIT = false
|
@AppStorage("autoQuitAfterEnablingJIT") private var doAutoQuitAfterEnablingJIT = false
|
||||||
|
|
@ -27,7 +83,7 @@ struct HomeView: View {
|
||||||
|
|
||||||
@State var scriptViewShow = false
|
@State var scriptViewShow = false
|
||||||
@State private var isShowingConsole = false
|
@State private var isShowingConsole = false
|
||||||
@AppStorage("DefaultScriptName") var selectedScript = "attachDetach.js"
|
@AppStorage(UserDefaults.Keys.defaultScriptName) var selectedScript = UserDefaults.Keys.defaultScriptNameValue
|
||||||
@State var jsModel: RunJSViewModel?
|
@State var jsModel: RunJSViewModel?
|
||||||
@ObservedObject private var mounting = MountingProgress.shared
|
@ObservedObject private var mounting = MountingProgress.shared
|
||||||
|
|
||||||
|
|
@ -83,7 +139,7 @@ struct HomeView: View {
|
||||||
config.scriptName = scriptName
|
config.scriptName = scriptName
|
||||||
}
|
}
|
||||||
if config.scriptData == nil, let bundleID = config.bundleID,
|
if config.scriptData == nil, let bundleID = config.bundleID,
|
||||||
let scriptInfo = preferredScript(for: bundleID) {
|
let scriptInfo = ScriptStore.preferredScript(for: bundleID) {
|
||||||
config.scriptData = scriptInfo.data
|
config.scriptData = scriptInfo.data
|
||||||
config.scriptName = scriptInfo.name
|
config.scriptName = scriptInfo.name
|
||||||
}
|
}
|
||||||
|
|
@ -121,33 +177,25 @@ struct HomeView: View {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.fileImporter(isPresented: $isShowingPairingFilePicker, allowedContentTypes: [UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!, UTType(filenameExtension: "mobiledevicepair", conformingTo: .data)!, .propertyList]) { result in
|
.fileImporter(isPresented: $isShowingPairingFilePicker, allowedContentTypes: PairingFileStore.supportedContentTypes) { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let url):
|
case .success(let url):
|
||||||
let fileManager = FileManager.default
|
let fileManager = FileManager.default
|
||||||
let accessing = url.startAccessingSecurityScopedResource()
|
do {
|
||||||
if fileManager.fileExists(atPath: url.path) {
|
try PairingFileStore.importFromPicker(url, fileManager: fileManager)
|
||||||
do {
|
pubTunnelConnected = false
|
||||||
let dest = URL.documentsDirectory.appendingPathComponent("pairingFile.plist")
|
startTunnelInBackground()
|
||||||
if fileManager.fileExists(atPath: dest.path) {
|
NotificationCenter.default.post(name: .pairingFileImported, object: nil)
|
||||||
try fileManager.removeItem(at: dest)
|
// Dismiss any existing connection error alert
|
||||||
}
|
if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||||
try fileManager.copyItem(at: url, to: dest)
|
let root = scene.windows.first?.rootViewController {
|
||||||
pubTunnelConnected = false
|
var top = root
|
||||||
startTunnelInBackground()
|
while let presented = top.presentedViewController { top = presented }
|
||||||
NotificationCenter.default.post(name: .pairingFileImported, object: nil)
|
if top is UIAlertController { top.dismiss(animated: true) }
|
||||||
// Dismiss any existing connection error alert
|
|
||||||
if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
|
||||||
let root = scene.windows.first?.rootViewController {
|
|
||||||
var top = root
|
|
||||||
while let presented = top.presentedViewController { top = presented }
|
|
||||||
if top is UIAlertController { top.dismiss(animated: true) }
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
print("Error copying pairing file: \(error)")
|
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
print("Error copying pairing file: \(error)")
|
||||||
}
|
}
|
||||||
if accessing { url.stopAccessingSecurityScopedResource() }
|
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
print("Failed to import pairing file: \(error)")
|
print("Failed to import pairing file: \(error)")
|
||||||
}
|
}
|
||||||
|
|
@ -179,70 +227,6 @@ struct HomeView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private func autoScript(for bundleID: String) -> (data: Data, name: String)? {
|
|
||||||
guard ProcessInfo.processInfo.hasTXM else { return nil }
|
|
||||||
guard #available(iOS 26, *) else { return nil }
|
|
||||||
let appName = (try? JITEnableContext.shared.getAppList()[bundleID]) ?? storedFavoriteName(for: bundleID)
|
|
||||||
guard let appName,
|
|
||||||
let resource = autoScriptResource(for: appName) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
let scriptsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
|
||||||
.appendingPathComponent("scripts")
|
|
||||||
let documentsURL = scriptsDir.appendingPathComponent(resource.fileName)
|
|
||||||
if let data = try? Data(contentsOf: documentsURL) {
|
|
||||||
return (data, resource.fileName)
|
|
||||||
}
|
|
||||||
guard let bundleURL = Bundle.main.url(forResource: resource.resource, withExtension: "js"),
|
|
||||||
let data = try? Data(contentsOf: bundleURL) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return (data, resource.fileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func assignedScript(for bundleID: String) -> (data: Data, name: String)? {
|
|
||||||
guard let mapping = UserDefaults.standard.dictionary(forKey: "BundleScriptMap") as? [String: String],
|
|
||||||
let scriptName = mapping[bundleID] else { return nil }
|
|
||||||
let scriptsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
|
||||||
.appendingPathComponent("scripts")
|
|
||||||
let scriptURL = scriptsDir.appendingPathComponent(scriptName)
|
|
||||||
guard FileManager.default.fileExists(atPath: scriptURL.path),
|
|
||||||
let data = try? Data(contentsOf: scriptURL) else { return nil }
|
|
||||||
return (data, scriptName)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func preferredScript(for bundleID: String) -> (data: Data, name: String)? {
|
|
||||||
if let assigned = assignedScript(for: bundleID) {
|
|
||||||
return assigned
|
|
||||||
}
|
|
||||||
return autoScript(for: bundleID)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func storedFavoriteName(for bundleID: String) -> String? {
|
|
||||||
let defaults = UserDefaults(suiteName: "group.com.stik.sj")
|
|
||||||
let names = defaults?.dictionary(forKey: "favoriteAppNames") as? [String: String]
|
|
||||||
return names?[bundleID]
|
|
||||||
}
|
|
||||||
|
|
||||||
private func autoScriptResource(for appName: String) -> (resource: String, fileName: String)? {
|
|
||||||
switch appName {
|
|
||||||
case "maciOS":
|
|
||||||
return ("maciOS", "maciOS.js")
|
|
||||||
case "Amethyst", "MeloNX", "XeniOS", "MeloCafe":
|
|
||||||
return ("universal", "universal.js")
|
|
||||||
case "Geode":
|
|
||||||
return ("Geode", "Geode.js")
|
|
||||||
case "Manic EMU":
|
|
||||||
return ("manic", "manic.js")
|
|
||||||
case "UTM", "DolphiniOS", "Flycast":
|
|
||||||
return ("UTM-Dolphin", "UTM-Dolphin.js")
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getJsCallback(_ script: Data, name: String? = nil) -> DebugAppCallback {
|
private func getJsCallback(_ script: Data, name: String? = nil) -> DebugAppCallback {
|
||||||
return { pid, debugProxyHandle, remoteServerHandle, semaphore in
|
return { pid, debugProxyHandle, remoteServerHandle, semaphore in
|
||||||
let model = RunJSViewModel(pid: Int(pid),
|
let model = RunJSViewModel(pid: Int(pid),
|
||||||
|
|
@ -255,9 +239,13 @@ struct HomeView: View {
|
||||||
scriptViewShow = true
|
scriptViewShow = true
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.global(qos: .background).async {
|
do {
|
||||||
do { try model.runScript(data: script, name: name) }
|
try model.runScript(data: script, name: name)
|
||||||
catch { showAlert(title: "Error Occurred While Executing Script.".localized, message: error.localizedDescription, showOk: true) }
|
} catch {
|
||||||
|
semaphore.signal()
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
showAlert(title: "Error Occurred While Executing Script.".localized, message: error.localizedDescription, showOk: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -265,7 +253,6 @@ struct HomeView: View {
|
||||||
private func startJITInBackground(bundleID: String? = nil, pid: Int? = nil, scriptData: Data? = nil, scriptName: String? = nil, triggeredByURLScheme: Bool = false) {
|
private func startJITInBackground(bundleID: String? = nil, pid: Int? = nil, scriptData: Data? = nil, scriptName: String? = nil, triggeredByURLScheme: Bool = false) {
|
||||||
isProcessing = true
|
isProcessing = true
|
||||||
LogManager.shared.addInfoLog("Starting Debug for \(bundleID ?? String(pid ?? 0))")
|
LogManager.shared.addInfoLog("Starting Debug for \(bundleID ?? String(pid ?? 0))")
|
||||||
BackgroundLocationManager.shared.requestStart()
|
|
||||||
|
|
||||||
if triggeredByURLScheme {
|
if triggeredByURLScheme {
|
||||||
pubTunnelConnected = false
|
pubTunnelConnected = false
|
||||||
|
|
@ -273,6 +260,8 @@ struct HomeView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.global(qos: .background).async {
|
DispatchQueue.global(qos: .background).async {
|
||||||
|
let keepAliveLease = DebugKeepAliveLease()
|
||||||
|
defer { keepAliveLease.invalidate() }
|
||||||
|
|
||||||
if triggeredByURLScheme {
|
if triggeredByURLScheme {
|
||||||
sleep(1)
|
sleep(1)
|
||||||
|
|
@ -280,7 +269,6 @@ struct HomeView: View {
|
||||||
let finishProcessing = {
|
let finishProcessing = {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
isProcessing = false
|
isProcessing = false
|
||||||
BackgroundLocationManager.shared.requestStop()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -288,7 +276,7 @@ struct HomeView: View {
|
||||||
var scriptName = scriptName
|
var scriptName = scriptName
|
||||||
if scriptData == nil,
|
if scriptData == nil,
|
||||||
let bundleID,
|
let bundleID,
|
||||||
let preferred = preferredScript(for: bundleID) {
|
let preferred = ScriptStore.preferredScript(for: bundleID) {
|
||||||
scriptName = preferred.name
|
scriptName = preferred.name
|
||||||
scriptData = preferred.data
|
scriptData = preferred.data
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import Combine
|
||||||
struct InstalledAppsListView: View {
|
struct InstalledAppsListView: View {
|
||||||
@StateObject private var viewModel = InstalledAppsViewModel()
|
@StateObject private var viewModel = InstalledAppsViewModel()
|
||||||
|
|
||||||
private let sharedDefaults = UserDefaults(suiteName: "group.com.stik.sj") ?? .standard
|
private let sharedDefaults = UserDefaults(suiteName: ScriptStore.favoriteAppNamesSuiteName) ?? .standard
|
||||||
|
|
||||||
@AppStorage("recentApps") private var recentApps: [String] = []
|
@AppStorage("recentApps") private var recentApps: [String] = []
|
||||||
@AppStorage("favoriteApps") private var favoriteApps: [String] = [] {
|
@AppStorage("favoriteApps") private var favoriteApps: [String] = [] {
|
||||||
|
|
@ -414,7 +414,7 @@ private enum AppListTab: Int, CaseIterable, Identifiable {
|
||||||
let prevF = (sharedDefaults.array(forKey: "favoriteApps") as? [String]) ?? []
|
let prevF = (sharedDefaults.array(forKey: "favoriteApps") as? [String]) ?? []
|
||||||
let prevPinned = (sharedDefaults.array(forKey: "pinnedSystemApps") as? [String]) ?? []
|
let prevPinned = (sharedDefaults.array(forKey: "pinnedSystemApps") as? [String]) ?? []
|
||||||
let prevPinnedNames = (sharedDefaults.dictionary(forKey: "pinnedSystemAppNames") as? [String: String]) ?? [:]
|
let prevPinnedNames = (sharedDefaults.dictionary(forKey: "pinnedSystemAppNames") as? [String: String]) ?? [:]
|
||||||
let prevFavNames = (sharedDefaults.dictionary(forKey: "favoriteAppNames") as? [String: String]) ?? [:]
|
let prevFavNames = (sharedDefaults.dictionary(forKey: ScriptStore.favoriteAppNamesKey) as? [String: String]) ?? [:]
|
||||||
|
|
||||||
if prevR != recentApps {
|
if prevR != recentApps {
|
||||||
sharedDefaults.set(recentApps, forKey: "recentApps")
|
sharedDefaults.set(recentApps, forKey: "recentApps")
|
||||||
|
|
@ -441,7 +441,7 @@ private enum AppListTab: Int, CaseIterable, Identifiable {
|
||||||
return (id, name)
|
return (id, name)
|
||||||
})
|
})
|
||||||
if prevFavNames != computedFavNames {
|
if prevFavNames != computedFavNames {
|
||||||
sharedDefaults.set(computedFavNames, forKey: "favoriteAppNames")
|
sharedDefaults.set(computedFavNames, forKey: ScriptStore.favoriteAppNamesKey)
|
||||||
touched = true
|
touched = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -693,16 +693,14 @@ struct AppButton: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func assignScript(_ url: URL?) {
|
private func assignScript(_ url: URL?) {
|
||||||
var mapping = UserDefaults.standard.dictionary(forKey: "BundleScriptMap") as? [String: String] ?? [:]
|
|
||||||
if let url {
|
if let url {
|
||||||
let filename = url.lastPathComponent
|
let filename = url.lastPathComponent
|
||||||
mapping[bundleID] = filename
|
ScriptStore.updateAssignedScriptName(filename, for: bundleID)
|
||||||
assignedScriptName = filename
|
assignedScriptName = filename
|
||||||
} else {
|
} else {
|
||||||
mapping.removeValue(forKey: bundleID)
|
ScriptStore.updateAssignedScriptName(nil, for: bundleID)
|
||||||
assignedScriptName = nil
|
assignedScriptName = nil
|
||||||
}
|
}
|
||||||
UserDefaults.standard.set(mapping, forKey: "BundleScriptMap")
|
|
||||||
Haptics.light()
|
Haptics.light()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -711,8 +709,7 @@ struct AppButton: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func currentAssignment(for bundleID: String) -> String? {
|
private static func currentAssignment(for bundleID: String) -> String? {
|
||||||
let mapping = UserDefaults.standard.dictionary(forKey: "BundleScriptMap") as? [String: String]
|
ScriptStore.assignedScriptName(for: bundleID)
|
||||||
return mapping?[bundleID]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func persistIfChanged() {
|
private func persistIfChanged() {
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ struct MainTabView: View {
|
||||||
@State private var didSetInitialHome = false
|
@State private var didSetInitialHome = false
|
||||||
|
|
||||||
private var configurableTabs: [TabDescriptor] {
|
private var configurableTabs: [TabDescriptor] {
|
||||||
var tabs: [TabDescriptor] = [
|
let tabs: [TabDescriptor] = [
|
||||||
TabDescriptor(id: "home", title: "Apps", systemImage: "square.grid.2x2") { AnyView(HomeView()) },
|
TabDescriptor(id: "home", title: "Apps", systemImage: "square.grid.2x2") { AnyView(HomeView()) },
|
||||||
TabDescriptor(id: "scripts", title: "Scripts", systemImage: "scroll") { AnyView(ScriptListView()) },
|
TabDescriptor(id: "scripts", title: "Scripts", systemImage: "scroll") { AnyView(ScriptListView()) },
|
||||||
TabDescriptor(id: "tools", title: "Tools", systemImage: "wrench.and.screwdriver") { AnyView(ToolsView()) },
|
TabDescriptor(id: "tools", title: "Tools", systemImage: "wrench.and.screwdriver") { AnyView(ToolsView()) },
|
||||||
|
|
@ -108,7 +108,7 @@ struct MainTabView: View {
|
||||||
switchObserver = nil
|
switchObserver = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: enabledTabIdentifiers) { _ in
|
.onChange(of: enabledTabIdentifiers) { _, _ in
|
||||||
ensureSelectionIsValid()
|
ensureSelectionIsValid()
|
||||||
}
|
}
|
||||||
.sheet(item: $detachedTab) { descriptor in
|
.sheet(item: $detachedTab) { descriptor in
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -32,10 +32,10 @@ struct ProcessInspectorView: View {
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
viewModel.stopAutoRefresh()
|
viewModel.stopAutoRefresh()
|
||||||
}
|
}
|
||||||
.alert(viewModel.killAlertTitle, isPresented: $viewModel.showKillAlert) {
|
.alert(viewModel.actionAlertTitle, isPresented: $viewModel.showActionAlert) {
|
||||||
Button("OK", role: .cancel) { }
|
Button("OK", role: .cancel) { }
|
||||||
} message: {
|
} message: {
|
||||||
Text(viewModel.killAlertMessage)
|
Text(viewModel.actionAlertMessage)
|
||||||
}
|
}
|
||||||
.alert(viewModel.errorAlertTitle, isPresented: $viewModel.showErrorAlert) {
|
.alert(viewModel.errorAlertTitle, isPresented: $viewModel.showErrorAlert) {
|
||||||
Button("Try Again") { viewModel.refresh() }
|
Button("Try Again") { viewModel.refresh() }
|
||||||
|
|
@ -63,8 +63,11 @@ struct ProcessInspectorView: View {
|
||||||
ForEach(viewModel.filteredProcesses) { process in
|
ForEach(viewModel.filteredProcesses) { process in
|
||||||
ProcessRow(
|
ProcessRow(
|
||||||
process: process,
|
process: process,
|
||||||
isKilling: viewModel.killingPID == process.pid,
|
activeControl: viewModel.activeControl(for: process),
|
||||||
|
isBusy: viewModel.isRunningControlAction,
|
||||||
isConfirming: killCandidate?.pid == process.pid,
|
isConfirming: killCandidate?.pid == process.pid,
|
||||||
|
onResumeTap: { viewModel.control(.resume, process: $0) },
|
||||||
|
onPauseTap: { viewModel.control(.pause, process: $0) },
|
||||||
onKillTap: { handleKillTap(for: $0) }
|
onKillTap: { handleKillTap(for: $0) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -82,7 +85,7 @@ private extension ProcessInspectorView {
|
||||||
killConfirmTask?.cancel()
|
killConfirmTask?.cancel()
|
||||||
killConfirmTask = nil
|
killConfirmTask = nil
|
||||||
killCandidate = nil
|
killCandidate = nil
|
||||||
viewModel.kill(process: process)
|
viewModel.control(.kill, process: process)
|
||||||
} else {
|
} else {
|
||||||
killCandidate = process
|
killCandidate = process
|
||||||
killConfirmTask?.cancel()
|
killConfirmTask?.cancel()
|
||||||
|
|
@ -100,10 +103,129 @@ private extension ProcessInspectorView {
|
||||||
|
|
||||||
// MARK: - Row
|
// MARK: - Row
|
||||||
|
|
||||||
|
enum ProcessControlAction: String {
|
||||||
|
case resume
|
||||||
|
case pause
|
||||||
|
case kill
|
||||||
|
|
||||||
|
var signal: Int32 {
|
||||||
|
switch self {
|
||||||
|
case .resume:
|
||||||
|
return Int32(SIGCONT)
|
||||||
|
case .pause:
|
||||||
|
return Int32(SIGSTOP)
|
||||||
|
case .kill:
|
||||||
|
return Int32(SIGKILL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var buttonLabel: String {
|
||||||
|
switch self {
|
||||||
|
case .resume:
|
||||||
|
return "Resume"
|
||||||
|
case .pause:
|
||||||
|
return "Pause"
|
||||||
|
case .kill:
|
||||||
|
return "Kill"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var systemImage: String {
|
||||||
|
switch self {
|
||||||
|
case .resume:
|
||||||
|
return "play.circle"
|
||||||
|
case .pause:
|
||||||
|
return "pause.circle"
|
||||||
|
case .kill:
|
||||||
|
return "xmark.circle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var tint: Color {
|
||||||
|
switch self {
|
||||||
|
case .resume:
|
||||||
|
return .green
|
||||||
|
case .pause:
|
||||||
|
return .orange
|
||||||
|
case .kill:
|
||||||
|
return .red
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var progressTitle: String {
|
||||||
|
switch self {
|
||||||
|
case .resume:
|
||||||
|
return "Resuming Process"
|
||||||
|
case .pause:
|
||||||
|
return "Pausing Process"
|
||||||
|
case .kill:
|
||||||
|
return "Terminating Process"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var timeoutTitle: String {
|
||||||
|
switch self {
|
||||||
|
case .resume:
|
||||||
|
return "Resume Timed Out"
|
||||||
|
case .pause:
|
||||||
|
return "Pause Timed Out"
|
||||||
|
case .kill:
|
||||||
|
return "Kill Timed Out"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var failureTitle: String {
|
||||||
|
switch self {
|
||||||
|
case .resume:
|
||||||
|
return "Resume Failed"
|
||||||
|
case .pause:
|
||||||
|
return "Pause Failed"
|
||||||
|
case .kill:
|
||||||
|
return "Kill Failed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var successTitle: String {
|
||||||
|
switch self {
|
||||||
|
case .resume:
|
||||||
|
return "Process Resumed"
|
||||||
|
case .pause:
|
||||||
|
return "Process Paused"
|
||||||
|
case .kill:
|
||||||
|
return "Process Terminated"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func successMessage(for pid: Int) -> String {
|
||||||
|
switch self {
|
||||||
|
case .resume:
|
||||||
|
return "Sent SIGCONT (19) to PID \(pid)."
|
||||||
|
case .pause:
|
||||||
|
return "Sent SIGSTOP (17) to PID \(pid)."
|
||||||
|
case .kill:
|
||||||
|
return "PID \(pid) was terminated."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func timeoutMessage(for pid: Int) -> String {
|
||||||
|
switch self {
|
||||||
|
case .resume:
|
||||||
|
return "Could not confirm resume for PID \(pid). Try again."
|
||||||
|
case .pause:
|
||||||
|
return "Could not confirm pause for PID \(pid). Try again."
|
||||||
|
case .kill:
|
||||||
|
return "Could not confirm termination for PID \(pid). Try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private struct ProcessRow: View {
|
private struct ProcessRow: View {
|
||||||
let process: ProcessInfoEntry
|
let process: ProcessInfoEntry
|
||||||
let isKilling: Bool
|
let activeControl: ProcessControlAction?
|
||||||
|
let isBusy: Bool
|
||||||
let isConfirming: Bool
|
let isConfirming: Bool
|
||||||
|
let onResumeTap: (ProcessInfoEntry) -> Void
|
||||||
|
let onPauseTap: (ProcessInfoEntry) -> Void
|
||||||
let onKillTap: (ProcessInfoEntry) -> Void
|
let onKillTap: (ProcessInfoEntry) -> Void
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
@ -128,27 +250,54 @@ private struct ProcessRow: View {
|
||||||
.textSelection(.enabled)
|
.textSelection(.enabled)
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
if isKilling {
|
if activeControl != nil {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
.progressViewStyle(.circular)
|
.progressViewStyle(.circular)
|
||||||
.tint(.accentColor)
|
.tint(.accentColor)
|
||||||
} else {
|
} else {
|
||||||
Button {
|
HStack(spacing: 8) {
|
||||||
onKillTap(process)
|
Button {
|
||||||
} label: {
|
onResumeTap(process)
|
||||||
if isConfirming {
|
} label: {
|
||||||
Label("Confirm", systemImage: "checkmark.circle.fill")
|
Image(systemName: ProcessControlAction.resume.systemImage)
|
||||||
.labelStyle(.iconOnly)
|
|
||||||
.font(.title3)
|
|
||||||
} else {
|
|
||||||
Image(systemName: "xmark.circle")
|
|
||||||
.font(.title3)
|
.font(.title3)
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.controlSize(.small)
|
||||||
|
.tint(ProcessControlAction.resume.tint)
|
||||||
|
.labelStyle(.iconOnly)
|
||||||
|
.disabled(isBusy)
|
||||||
|
|
||||||
|
Button {
|
||||||
|
onPauseTap(process)
|
||||||
|
} label: {
|
||||||
|
Image(systemName: ProcessControlAction.pause.systemImage)
|
||||||
|
.font(.title3)
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.controlSize(.small)
|
||||||
|
.tint(ProcessControlAction.pause.tint)
|
||||||
|
.labelStyle(.iconOnly)
|
||||||
|
.disabled(isBusy)
|
||||||
|
|
||||||
|
Button {
|
||||||
|
onKillTap(process)
|
||||||
|
} label: {
|
||||||
|
if isConfirming {
|
||||||
|
Label("Confirm", systemImage: "checkmark.circle.fill")
|
||||||
|
.labelStyle(.iconOnly)
|
||||||
|
.font(.title3)
|
||||||
|
} else {
|
||||||
|
Image(systemName: ProcessControlAction.kill.systemImage)
|
||||||
|
.font(.title3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.controlSize(.small)
|
||||||
|
.tint(isConfirming ? .green : ProcessControlAction.kill.tint)
|
||||||
|
.labelStyle(.iconOnly)
|
||||||
|
.disabled(isBusy)
|
||||||
}
|
}
|
||||||
.buttonStyle(.bordered)
|
|
||||||
.controlSize(.small)
|
|
||||||
.tint(isConfirming ? .green : .red)
|
|
||||||
.labelStyle(.iconOnly)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -166,13 +315,13 @@ final class ProcessInspectorViewModel: ObservableObject {
|
||||||
@Published var showErrorAlert = false
|
@Published var showErrorAlert = false
|
||||||
@Published var errorAlertTitle = ""
|
@Published var errorAlertTitle = ""
|
||||||
@Published var errorAlertMessage = ""
|
@Published var errorAlertMessage = ""
|
||||||
@Published private(set) var killingPID: Int?
|
@Published private(set) var activeControlState: (pid: Int, action: ProcessControlAction)?
|
||||||
@Published var showKillAlert = false
|
@Published var showActionAlert = false
|
||||||
@Published var killAlertTitle = ""
|
@Published var actionAlertTitle = ""
|
||||||
@Published var killAlertMessage = ""
|
@Published var actionAlertMessage = ""
|
||||||
|
|
||||||
private var refreshTask: Task<Void, Never>?
|
private var refreshTask: Task<Void, Never>?
|
||||||
private var killTimeoutTask: Task<Void, Never>?
|
private var controlTimeoutTask: Task<Void, Never>?
|
||||||
@Published private(set) var lastUpdated: Date?
|
@Published private(set) var lastUpdated: Date?
|
||||||
var filteredProcesses: [ProcessInfoEntry] {
|
var filteredProcesses: [ProcessInfoEntry] {
|
||||||
guard !searchText.isEmpty else { return processes }
|
guard !searchText.isEmpty else { return processes }
|
||||||
|
|
@ -207,27 +356,25 @@ final class ProcessInspectorViewModel: ObservableObject {
|
||||||
func stopAutoRefresh() {
|
func stopAutoRefresh() {
|
||||||
refreshTask?.cancel()
|
refreshTask?.cancel()
|
||||||
refreshTask = nil
|
refreshTask = nil
|
||||||
killTimeoutTask?.cancel()
|
controlTimeoutTask?.cancel()
|
||||||
killTimeoutTask = nil
|
controlTimeoutTask = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func refresh() {
|
func refresh() {
|
||||||
guard !isRefreshing else { return }
|
guard !isRefreshing else { return }
|
||||||
isRefreshing = true
|
isRefreshing = true
|
||||||
Task.detached(priority: .utility) { [weak self] in
|
Task.detached(priority: .utility) { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
var err: NSError?
|
var err: NSError?
|
||||||
let entries = FetchDeviceProcessList(&err) ?? []
|
let parsedEntries = ProcessInfoEntry.currentEntries(&err)
|
||||||
|
let errorMessage = err?.localizedDescription
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
guard let self else { return }
|
if let errorMessage {
|
||||||
if let err {
|
|
||||||
self.errorAlertTitle = "Failed to Load Processes"
|
self.errorAlertTitle = "Failed to Load Processes"
|
||||||
self.errorAlertMessage = err.localizedDescription
|
self.errorAlertMessage = errorMessage
|
||||||
self.showErrorAlert = true
|
self.showErrorAlert = true
|
||||||
} else {
|
} else {
|
||||||
self.processes = entries.compactMap { item -> ProcessInfoEntry? in
|
self.processes = parsedEntries
|
||||||
guard let dict = item as? NSDictionary else { return nil }
|
|
||||||
return ProcessInfoEntry(dictionary: dict)
|
|
||||||
}
|
|
||||||
self.lastUpdated = Date()
|
self.lastUpdated = Date()
|
||||||
}
|
}
|
||||||
self.isRefreshing = false
|
self.isRefreshing = false
|
||||||
|
|
@ -235,84 +382,67 @@ final class ProcessInspectorViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func kill(process: ProcessInfoEntry) {
|
var isRunningControlAction: Bool {
|
||||||
guard killingPID == nil else {
|
activeControlState != nil
|
||||||
killAlertTitle = "Busy"
|
}
|
||||||
killAlertMessage = "Already terminating PID \(killingPID!)."
|
|
||||||
showKillAlert = true
|
func activeControl(for process: ProcessInfoEntry) -> ProcessControlAction? {
|
||||||
|
guard activeControlState?.pid == process.pid else { return nil }
|
||||||
|
return activeControlState?.action
|
||||||
|
}
|
||||||
|
|
||||||
|
func control(_ action: ProcessControlAction, process: ProcessInfoEntry) {
|
||||||
|
guard activeControlState == nil else {
|
||||||
|
actionAlertTitle = "Busy"
|
||||||
|
if let activeControlState {
|
||||||
|
actionAlertMessage = "\(activeControlState.action.progressTitle) for PID \(activeControlState.pid)."
|
||||||
|
} else {
|
||||||
|
actionAlertMessage = "Another process action is already running."
|
||||||
|
}
|
||||||
|
showActionAlert = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let targetPID = process.pid
|
let targetPID = process.pid
|
||||||
killingPID = targetPID
|
activeControlState = (targetPID, action)
|
||||||
killTimeoutTask?.cancel()
|
controlTimeoutTask?.cancel()
|
||||||
killTimeoutTask = Task { [weak self] in
|
controlTimeoutTask = Task { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
try? await Task.sleep(for: .seconds(8))
|
try? await Task.sleep(for: .seconds(8))
|
||||||
await MainActor.run {
|
if self.activeControlState?.pid == targetPID && self.activeControlState?.action == action {
|
||||||
guard let self else { return }
|
self.activeControlState = nil
|
||||||
if self.killingPID == targetPID {
|
self.actionAlertTitle = action.timeoutTitle
|
||||||
self.killingPID = nil
|
self.actionAlertMessage = action.timeoutMessage(for: targetPID)
|
||||||
self.killAlertTitle = "Kill Timed Out"
|
self.showActionAlert = true
|
||||||
self.killAlertMessage = "Could not confirm termination for PID \(targetPID). Try again."
|
|
||||||
self.showKillAlert = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Task.detached(priority: .userInitiated) { [weak self] in
|
Task.detached(priority: .userInitiated) { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
var err: NSError?
|
var err: NSError?
|
||||||
let success = KillDeviceProcess(Int32(targetPID), &err)
|
let success: Bool
|
||||||
|
do {
|
||||||
|
try JITEnableContext.shared.sendSignal(action.signal, toProcessWithPID: Int32(targetPID))
|
||||||
|
success = true
|
||||||
|
} catch let nsError as NSError {
|
||||||
|
err = nsError
|
||||||
|
success = false
|
||||||
|
}
|
||||||
|
let errorMessage = err?.localizedDescription ?? "Unknown error"
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
guard let self else { return }
|
self.controlTimeoutTask?.cancel()
|
||||||
self.killTimeoutTask?.cancel()
|
self.controlTimeoutTask = nil
|
||||||
self.killTimeoutTask = nil
|
guard self.activeControlState?.pid == targetPID && self.activeControlState?.action == action else { return }
|
||||||
guard self.killingPID == targetPID else { return }
|
self.activeControlState = nil
|
||||||
self.killingPID = nil
|
|
||||||
if success {
|
if success {
|
||||||
self.killAlertTitle = "Process Terminated"
|
self.actionAlertTitle = action.successTitle
|
||||||
self.killAlertMessage = "PID \(targetPID) was terminated."
|
self.actionAlertMessage = action.successMessage(for: targetPID)
|
||||||
self.showKillAlert = true
|
self.showActionAlert = true
|
||||||
self.refresh()
|
self.refresh()
|
||||||
} else {
|
} else {
|
||||||
self.killAlertTitle = "Kill Failed"
|
self.actionAlertTitle = action.failureTitle
|
||||||
self.killAlertMessage = err?.localizedDescription ?? "Unknown error"
|
self.actionAlertMessage = errorMessage
|
||||||
self.showKillAlert = true
|
self.showActionAlert = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ProcessInfoEntry: Identifiable {
|
|
||||||
let pid: Int
|
|
||||||
private let rawPath: String
|
|
||||||
let bundleID: String?
|
|
||||||
let name: String?
|
|
||||||
|
|
||||||
init?(dictionary: NSDictionary) {
|
|
||||||
guard let pidNumber = dictionary["pid"] as? NSNumber else { return nil }
|
|
||||||
pid = pidNumber.intValue
|
|
||||||
rawPath = dictionary["path"] as? String ?? "Unknown"
|
|
||||||
bundleID = dictionary["bundleID"] as? String
|
|
||||||
name = dictionary["name"] as? String
|
|
||||||
}
|
|
||||||
|
|
||||||
var id: Int { pid }
|
|
||||||
|
|
||||||
var executablePath: String {
|
|
||||||
rawPath.replacingOccurrences(of: "file://", with: "")
|
|
||||||
}
|
|
||||||
|
|
||||||
var displayName: String {
|
|
||||||
if let name = name, !name.isEmpty {
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
if let bundle = bundleID, !bundle.isEmpty {
|
|
||||||
return bundle
|
|
||||||
}
|
|
||||||
let cleaned = executablePath
|
|
||||||
if let component = cleaned.split(separator: "/").last {
|
|
||||||
return String(component)
|
|
||||||
}
|
|
||||||
return "Process \(pid)"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
// Created by Stephen on 3/27/25.
|
// Created by Stephen on 3/27/25.
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UniformTypeIdentifiers
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
|
|
@ -187,7 +186,7 @@ struct SettingsView: View {
|
||||||
}
|
}
|
||||||
.fileImporter(
|
.fileImporter(
|
||||||
isPresented: $isShowingPairingFilePicker,
|
isPresented: $isShowingPairingFilePicker,
|
||||||
allowedContentTypes: [UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!, UTType(filenameExtension: "mobiledevicepair", conformingTo: .data)!, .propertyList],
|
allowedContentTypes: PairingFileStore.supportedContentTypes,
|
||||||
allowsMultipleSelection: false
|
allowsMultipleSelection: false
|
||||||
) { result in
|
) { result in
|
||||||
switch result {
|
switch result {
|
||||||
|
|
@ -195,43 +194,32 @@ struct SettingsView: View {
|
||||||
guard let url = urls.first else { return }
|
guard let url = urls.first else { return }
|
||||||
|
|
||||||
let fileManager = FileManager.default
|
let fileManager = FileManager.default
|
||||||
let accessing = url.startAccessingSecurityScopedResource()
|
do {
|
||||||
|
try PairingFileStore.importFromPicker(url, fileManager: fileManager)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
isImportingFile = true
|
||||||
|
importProgress = 0.0
|
||||||
|
pairingStatusMessage = nil
|
||||||
|
showPairingFileMessage = false
|
||||||
|
}
|
||||||
|
|
||||||
if fileManager.fileExists(atPath: url.path) {
|
let progressTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
|
||||||
do {
|
|
||||||
if fileManager.fileExists(atPath: URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path) {
|
|
||||||
try fileManager.removeItem(at: URL.documentsDirectory.appendingPathComponent("pairingFile.plist"))
|
|
||||||
}
|
|
||||||
|
|
||||||
try fileManager.copyItem(at: url, to: URL.documentsDirectory.appendingPathComponent("pairingFile.plist"))
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
isImportingFile = true
|
if importProgress < 1.0 {
|
||||||
importProgress = 0.0
|
importProgress += 0.05
|
||||||
pairingStatusMessage = nil
|
} else {
|
||||||
showPairingFileMessage = false
|
timer.invalidate()
|
||||||
}
|
isImportingFile = false
|
||||||
|
|
||||||
let progressTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
if importProgress < 1.0 {
|
|
||||||
importProgress += 0.05
|
|
||||||
} else {
|
|
||||||
timer.invalidate()
|
|
||||||
isImportingFile = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
RunLoop.current.add(progressTimer, forMode: .common)
|
RunLoop.current.add(progressTimer, forMode: .common)
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
startTunnelInBackground()
|
startTunnelInBackground()
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
} catch { }
|
break
|
||||||
}
|
|
||||||
|
|
||||||
if accessing {
|
|
||||||
url.stopAccessingSecurityScopedResource()
|
|
||||||
}
|
}
|
||||||
case .failure:
|
case .failure:
|
||||||
break
|
break
|
||||||
|
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
//
|
|
||||||
// JITEnableContext.h
|
|
||||||
// StikJIT
|
|
||||||
//
|
|
||||||
// Created by s s on 2025/3/28.
|
|
||||||
//
|
|
||||||
@import Foundation;
|
|
||||||
@import UIKit;
|
|
||||||
#include "idevice.h"
|
|
||||||
#include "jit.h"
|
|
||||||
#include "mount.h"
|
|
||||||
|
|
||||||
typedef void (^LogFuncC)(const char* message, ...);
|
|
||||||
typedef void (^LogFunc)(NSString *message);
|
|
||||||
typedef void (^SyslogLineHandler)(NSString *line);
|
|
||||||
typedef void (^SyslogErrorHandler)(NSError *error);
|
|
||||||
|
|
||||||
@interface JITEnableContext : NSObject {
|
|
||||||
// tunnel
|
|
||||||
@protected AdapterHandle *adapter;
|
|
||||||
@protected RsdHandshakeHandle *handshake;
|
|
||||||
|
|
||||||
// process
|
|
||||||
@protected dispatch_queue_t processInspectorQueue;
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
- (RpPairingFileHandle*)getPairingFileWithError:(NSError**)error;
|
|
||||||
- (BOOL)ensureTunnelWithError:(NSError**)err;
|
|
||||||
- (BOOL)startTunnel:(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;
|
|
||||||
- (BOOL)launchAppWithoutDebug:(NSString*)bundleID logger:(LogFunc)logger;
|
|
||||||
@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;
|
|
||||||
- (NSArray<NSDictionary*>*)getSideloadedAppsWithError:(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
|
|
||||||
|
|
@ -1,201 +0,0 @@
|
||||||
//
|
|
||||||
// JITEnableContext.m
|
|
||||||
// StikJIT
|
|
||||||
//
|
|
||||||
// Created by s s on 2025/3/28.
|
|
||||||
//
|
|
||||||
#include "idevice.h"
|
|
||||||
#include <arpa/inet.h>
|
|
||||||
#include <signal.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
|
|
||||||
#include "jit.h"
|
|
||||||
#include "applist.h"
|
|
||||||
#include "profiles.h"
|
|
||||||
|
|
||||||
#include "JITEnableContext.h"
|
|
||||||
#import "StikDebug-Swift.h"
|
|
||||||
#include <os/lock.h>
|
|
||||||
#import <pthread.h>
|
|
||||||
|
|
||||||
static JITEnableContext* sharedJITContext = nil;
|
|
||||||
|
|
||||||
@implementation JITEnableContext {
|
|
||||||
NSError* lastTunnelError;
|
|
||||||
os_unfair_lock tunnelLock;
|
|
||||||
BOOL tunnelConnecting;
|
|
||||||
dispatch_semaphore_t tunnelSemaphore;
|
|
||||||
}
|
|
||||||
|
|
||||||
+ (instancetype)shared {
|
|
||||||
static dispatch_once_t onceToken;
|
|
||||||
dispatch_once(&onceToken, ^{
|
|
||||||
sharedJITContext = [[JITEnableContext alloc] init];
|
|
||||||
});
|
|
||||||
return sharedJITContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (instancetype)init {
|
|
||||||
NSFileManager* fm = [NSFileManager defaultManager];
|
|
||||||
NSURL* docPathUrl = [fm URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].firstObject;
|
|
||||||
NSURL* logURL = [docPathUrl URLByAppendingPathComponent:@"idevice_log.txt"];
|
|
||||||
idevice_init_logger(Info, Debug, (char*)logURL.path.UTF8String);
|
|
||||||
syslogQueue = dispatch_queue_create("com.stik.syslogrelay.queue", DISPATCH_QUEUE_SERIAL);
|
|
||||||
syslogStreaming = NO;
|
|
||||||
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);
|
|
||||||
|
|
||||||
tunnelLock = OS_UNFAIR_LOCK_INIT;
|
|
||||||
tunnelConnecting = NO;
|
|
||||||
tunnelSemaphore = NULL;
|
|
||||||
lastTunnelError = nil;
|
|
||||||
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSError*)errorWithStr:(NSString*)str code:(int)code {
|
|
||||||
return [NSError errorWithDomain:@"StikJIT"
|
|
||||||
code:code
|
|
||||||
userInfo:@{ NSLocalizedDescriptionKey: str }];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (LogFuncC)createCLogger:(LogFunc)logger {
|
|
||||||
return ^(const char* format, ...) {
|
|
||||||
va_list args;
|
|
||||||
va_start(args, format);
|
|
||||||
NSString* fmt = [NSString stringWithCString:format encoding:NSASCIIStringEncoding];
|
|
||||||
NSString* message = [[NSString alloc] initWithFormat:fmt arguments:args];
|
|
||||||
|
|
||||||
if ([message containsString:@"ERROR"] || [message containsString:@"Error"]) {
|
|
||||||
[[LogManagerBridge shared] addErrorLog:message];
|
|
||||||
} else if ([message containsString:@"WARNING"] || [message containsString:@"Warning"]) {
|
|
||||||
[[LogManagerBridge shared] addWarningLog:message];
|
|
||||||
} else if ([message containsString:@"DEBUG"]) {
|
|
||||||
[[LogManagerBridge shared] addDebugLog:message];
|
|
||||||
} else {
|
|
||||||
[[LogManagerBridge shared] addInfoLog:message];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (logger) {
|
|
||||||
logger(message);
|
|
||||||
}
|
|
||||||
va_end(args);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
- (RpPairingFileHandle*)getPairingFileWithError:(NSError**)error {
|
|
||||||
NSFileManager* fm = [NSFileManager defaultManager];
|
|
||||||
NSURL* docPathUrl = [fm URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].firstObject;
|
|
||||||
NSURL* pairingFileURL = [docPathUrl URLByAppendingPathComponent:@"pairingFile.plist"];
|
|
||||||
|
|
||||||
if (![fm fileExistsAtPath:pairingFileURL.path]) {
|
|
||||||
*error = [self errorWithStr:@"Pairing file not found!" code:-17];
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
RpPairingFileHandle* pairingFile = NULL;
|
|
||||||
IdeviceFfiError* err = rp_pairing_file_read(pairingFileURL.fileSystemRepresentation, &pairingFile);
|
|
||||||
if (err) {
|
|
||||||
*error = [self errorWithStr:@"Failed to read pairing file!" code:err->code];
|
|
||||||
idevice_error_free(err);
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
return pairingFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)startTunnel:(NSError**)err {
|
|
||||||
os_unfair_lock_lock(&tunnelLock);
|
|
||||||
|
|
||||||
// If tunnel is already being created, wait for it to complete
|
|
||||||
if (tunnelConnecting) {
|
|
||||||
dispatch_semaphore_t waitSemaphore = tunnelSemaphore;
|
|
||||||
os_unfair_lock_unlock(&tunnelLock);
|
|
||||||
|
|
||||||
if (waitSemaphore) {
|
|
||||||
dispatch_semaphore_wait(waitSemaphore, DISPATCH_TIME_FOREVER);
|
|
||||||
dispatch_semaphore_signal(waitSemaphore);
|
|
||||||
}
|
|
||||||
*err = lastTunnelError;
|
|
||||||
return *err == nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark tunnel as connecting
|
|
||||||
tunnelConnecting = YES;
|
|
||||||
tunnelSemaphore = dispatch_semaphore_create(0);
|
|
||||||
dispatch_semaphore_t completionSemaphore = tunnelSemaphore;
|
|
||||||
os_unfair_lock_unlock(&tunnelLock);
|
|
||||||
|
|
||||||
RpPairingFileHandle* pairingFile = [self getPairingFileWithError:err];
|
|
||||||
if (*err) {
|
|
||||||
os_unfair_lock_lock(&tunnelLock);
|
|
||||||
tunnelConnecting = NO;
|
|
||||||
tunnelSemaphore = NULL;
|
|
||||||
os_unfair_lock_unlock(&tunnelLock);
|
|
||||||
dispatch_semaphore_signal(completionSemaphore);
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct sockaddr_in addr;
|
|
||||||
memset(&addr, 0, sizeof(addr));
|
|
||||||
addr.sin_family = AF_INET;
|
|
||||||
addr.sin_port = htons(49152);
|
|
||||||
|
|
||||||
NSString* deviceIP = [[NSUserDefaults standardUserDefaults] stringForKey:@"customTargetIP"];
|
|
||||||
inet_pton(AF_INET, (deviceIP && deviceIP.length > 0) ? [deviceIP UTF8String] : "10.7.0.1", &addr.sin_addr);
|
|
||||||
|
|
||||||
AdapterHandle *newAdapter = NULL;
|
|
||||||
RsdHandshakeHandle *newHandshake = NULL;
|
|
||||||
IdeviceFfiError *ffiErr = tunnel_create_rppairing(
|
|
||||||
(const idevice_sockaddr *)&addr,
|
|
||||||
sizeof(addr),
|
|
||||||
"StikDebug",
|
|
||||||
pairingFile,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
&newAdapter,
|
|
||||||
&newHandshake
|
|
||||||
);
|
|
||||||
rp_pairing_file_free(pairingFile);
|
|
||||||
|
|
||||||
if (ffiErr) {
|
|
||||||
*err = [self errorWithStr:[NSString stringWithUTF8String:ffiErr->message ?: "Failed to create tunnel"]
|
|
||||||
code:ffiErr->code];
|
|
||||||
lastTunnelError = *err;
|
|
||||||
idevice_error_free(ffiErr);
|
|
||||||
} else {
|
|
||||||
// Clean up old tunnel if any
|
|
||||||
if (handshake) { rsd_handshake_free(handshake); }
|
|
||||||
if (adapter) { adapter_free(adapter); }
|
|
||||||
adapter = newAdapter;
|
|
||||||
handshake = newHandshake;
|
|
||||||
lastTunnelError = nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
os_unfair_lock_lock(&tunnelLock);
|
|
||||||
tunnelConnecting = NO;
|
|
||||||
tunnelSemaphore = NULL;
|
|
||||||
os_unfair_lock_unlock(&tunnelLock);
|
|
||||||
dispatch_semaphore_signal(completionSemaphore);
|
|
||||||
|
|
||||||
return *err == nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)ensureTunnelWithError:(NSError**)err {
|
|
||||||
if (!adapter || !handshake) {
|
|
||||||
return [self startTunnel:err];
|
|
||||||
}
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)dealloc {
|
|
||||||
[self stopSyslogRelay];
|
|
||||||
if (handshake) {
|
|
||||||
rsd_handshake_free(handshake);
|
|
||||||
}
|
|
||||||
if (adapter) {
|
|
||||||
adapter_free(adapter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
//
|
|
||||||
// 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
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
//
|
|
||||||
// applist.h
|
|
||||||
// StikJIT
|
|
||||||
//
|
|
||||||
// Created by Stephen on 3/27/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
#ifndef APPLIST_H
|
|
||||||
#define APPLIST_H
|
|
||||||
@import Foundation;
|
|
||||||
@import UIKit;
|
|
||||||
|
|
||||||
NSDictionary<NSString*, NSString*>* list_installed_apps(AdapterHandle* adapter, RsdHandshakeHandle* handshake, NSString** error);
|
|
||||||
NSDictionary<NSString*, NSString*>* list_all_apps(AdapterHandle* adapter, RsdHandshakeHandle* handshake, NSString** error);
|
|
||||||
NSDictionary<NSString*, NSString*>* list_hidden_system_apps(AdapterHandle* adapter, RsdHandshakeHandle* handshake, NSString** error);
|
|
||||||
UIImage* getAppIcon(AdapterHandle* adapter, RsdHandshakeHandle* handshake, NSString* bundleID, NSString** error);
|
|
||||||
|
|
||||||
#endif /* APPLIST_H */
|
|
||||||
|
|
@ -1,363 +0,0 @@
|
||||||
//
|
|
||||||
// applist.c
|
|
||||||
// StikJIT
|
|
||||||
//
|
|
||||||
// Created by Stephen on 3/27/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "idevice.h"
|
|
||||||
#include <arpa/inet.h>
|
|
||||||
#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");
|
|
||||||
if (displayNameNode) {
|
|
||||||
char *displayNameC = NULL;
|
|
||||||
plist_get_string_val(displayNameNode, &displayNameC);
|
|
||||||
if (displayNameC && displayNameC[0] != '\0') {
|
|
||||||
NSString *displayName = [NSString stringWithUTF8String:displayNameC];
|
|
||||||
plist_mem_free(displayNameC);
|
|
||||||
return displayName;
|
|
||||||
}
|
|
||||||
plist_mem_free(displayNameC);
|
|
||||||
}
|
|
||||||
|
|
||||||
plist_t nameNode = plist_dict_get_item(app, "CFBundleName");
|
|
||||||
if (nameNode) {
|
|
||||||
char *nameC = NULL;
|
|
||||||
plist_get_string_val(nameNode, &nameC);
|
|
||||||
if (nameC && nameC[0] != '\0') {
|
|
||||||
NSString *name = [NSString stringWithUTF8String:nameC];
|
|
||||||
plist_mem_free(nameC);
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
plist_mem_free(nameC);
|
|
||||||
}
|
|
||||||
|
|
||||||
return @"Unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
static BOOL nodeContainsHiddenTag(plist_t tagsNode)
|
|
||||||
{
|
|
||||||
if (!tagsNode || plist_get_node_type(tagsNode) != PLIST_ARRAY) {
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t tagsCount = plist_array_get_size(tagsNode);
|
|
||||||
for (uint32_t i = 0; i < tagsCount; i++) {
|
|
||||||
plist_t tagNode = plist_array_get_item(tagsNode, i);
|
|
||||||
if (!tagNode || plist_get_node_type(tagNode) != PLIST_STRING) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
char *tagC = NULL;
|
|
||||||
plist_get_string_val(tagNode, &tagC);
|
|
||||||
if (!tagC) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
BOOL isHidden = (strcmp(tagC, "hidden") == 0 || strcmp(tagC, "hidden-system-app") == 0);
|
|
||||||
free(tagC);
|
|
||||||
if (isHidden) {
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
static BOOL isHiddenSystemApp(plist_t app)
|
|
||||||
{
|
|
||||||
plist_t typeNode = plist_dict_get_item(app, "ApplicationType");
|
|
||||||
BOOL isSystemType = NO;
|
|
||||||
if (typeNode && plist_get_node_type(typeNode) == PLIST_STRING) {
|
|
||||||
char *typeC = NULL;
|
|
||||||
plist_get_string_val(typeNode, &typeC);
|
|
||||||
if (typeC) {
|
|
||||||
if (strcmp(typeC, "System") == 0 || strcmp(typeC, "HiddenSystemApp") == 0) {
|
|
||||||
isSystemType = YES;
|
|
||||||
}
|
|
||||||
free(typeC);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isSystemType) {
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
plist_t hiddenNode = plist_dict_get_item(app, "IsHidden");
|
|
||||||
if (hiddenNode && plist_get_node_type(hiddenNode) == PLIST_BOOLEAN) {
|
|
||||||
uint8_t hidden = 0;
|
|
||||||
plist_get_bool_val(hiddenNode, &hidden);
|
|
||||||
if (hidden) {
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
plist_t tagsNode = plist_dict_get_item(app, "SBAppTags");
|
|
||||||
if (nodeContainsHiddenTag(tagsNode)) {
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
static NSDictionary<NSString*, NSString*> *buildAppDictionary(void *apps,
|
|
||||||
size_t count,
|
|
||||||
BOOL requireGetTaskAllow,
|
|
||||||
BOOL (^filter)(plist_t app))
|
|
||||||
{
|
|
||||||
NSMutableDictionary<NSString*, NSString*> *result = [NSMutableDictionary dictionaryWithCapacity:count];
|
|
||||||
|
|
||||||
for (size_t i = 0; i < count; i++) {
|
|
||||||
plist_t app = ((plist_t *)apps)[i];
|
|
||||||
plist_t ent = plist_dict_get_item(app, "Entitlements");
|
|
||||||
|
|
||||||
if (requireGetTaskAllow) {
|
|
||||||
if (!ent) continue;
|
|
||||||
plist_t tnode = plist_dict_get_item(ent, "get-task-allow");
|
|
||||||
if (!tnode) continue;
|
|
||||||
|
|
||||||
uint8_t isAllowed = 0;
|
|
||||||
plist_get_bool_val(tnode, &isAllowed);
|
|
||||||
if (!isAllowed) continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter && !filter(app)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
plist_t bidNode = plist_dict_get_item(app, "CFBundleIdentifier");
|
|
||||||
if (!bidNode) continue;
|
|
||||||
|
|
||||||
char *bidC = NULL;
|
|
||||||
plist_get_string_val(bidNode, &bidC);
|
|
||||||
if (!bidC || bidC[0] == '\0') {
|
|
||||||
plist_mem_free(bidC);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSString *bundleID = [NSString stringWithUTF8String:bidC];
|
|
||||||
plist_mem_free(bidC);
|
|
||||||
|
|
||||||
result[bundleID] = extractAppName(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
static NSArray<NSDictionary*>* getSideloadedApps(AdapterHandle *adapter,
|
|
||||||
RsdHandshakeHandle *handshake,
|
|
||||||
NSString **error) {
|
|
||||||
InstallationProxyClientHandle *client = NULL;
|
|
||||||
IdeviceFfiError* err = installation_proxy_connect_rsd(adapter, handshake, &client);
|
|
||||||
if (err) {
|
|
||||||
*error = [NSString stringWithFormat:@"Failed to connect to installation proxy: %s", err->message];
|
|
||||||
idevice_error_free(err);
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
plist_t *apps = NULL;
|
|
||||||
size_t count = 0;
|
|
||||||
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);
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSMutableArray<NSDictionary*>* result = [NSMutableArray new];
|
|
||||||
|
|
||||||
for (size_t i = 0; i < count; i++) {
|
|
||||||
plist_t app = ((plist_t *)apps)[i];
|
|
||||||
|
|
||||||
plist_t profileValidatedNode = 0;
|
|
||||||
if(!(profileValidatedNode = plist_dict_get_item(app, "ProfileValidated"))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
char* bin = 0;
|
|
||||||
uint32_t size = 0;
|
|
||||||
plist_to_bin(app, &bin, &size);
|
|
||||||
if(!bin || size == 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSData* d = [NSData dataWithBytes:bin length:size];
|
|
||||||
NSError* err;
|
|
||||||
NSDictionary* dict = [NSPropertyListSerialization propertyListWithData:d options:0 format:nil error:&err];
|
|
||||||
plist_mem_free(bin);
|
|
||||||
|
|
||||||
if(err) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
[result addObject:dict];
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
static NSDictionary<NSString*, NSString*> *performAppQuery(AdapterHandle *adapter,
|
|
||||||
RsdHandshakeHandle *handshake,
|
|
||||||
BOOL requireGetTaskAllow,
|
|
||||||
NSString **error,
|
|
||||||
BOOL (^filter)(plist_t app))
|
|
||||||
{
|
|
||||||
InstallationProxyClientHandle *client = NULL;
|
|
||||||
IdeviceFfiError* err = installation_proxy_connect_rsd(adapter, handshake, &client);
|
|
||||||
if (err) {
|
|
||||||
*error = [NSString stringWithFormat:@"Failed to connect to installation proxy: %s", err->message];
|
|
||||||
idevice_error_free(err);
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
plist_t *apps = NULL;
|
|
||||||
size_t count = 0;
|
|
||||||
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);
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSDictionary<NSString*, NSString*>* list_installed_apps(AdapterHandle* adapter, RsdHandshakeHandle* handshake, NSString** error) {
|
|
||||||
return performAppQuery(adapter, handshake, YES, error, nil);
|
|
||||||
}
|
|
||||||
|
|
||||||
NSDictionary<NSString*, NSString*>* list_all_apps(AdapterHandle* adapter, RsdHandshakeHandle* handshake, NSString** error) {
|
|
||||||
return performAppQuery(adapter, handshake, NO, error, nil);
|
|
||||||
}
|
|
||||||
|
|
||||||
NSDictionary<NSString*, NSString*>* list_hidden_system_apps(AdapterHandle* adapter, RsdHandshakeHandle* handshake, NSString** error) {
|
|
||||||
return performAppQuery(adapter, handshake, NO, error, ^BOOL(plist_t app) {
|
|
||||||
return isHiddenSystemApp(app);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
UIImage* getAppIcon(AdapterHandle* adapter, RsdHandshakeHandle* handshake, NSString* bundleID, NSString** error) {
|
|
||||||
SpringBoardServicesClientHandle *client = NULL;
|
|
||||||
IdeviceFfiError *err = springboard_services_connect_rsd(adapter, handshake, &client);
|
|
||||||
if (err) {
|
|
||||||
*error = [NSString stringWithUTF8String:err->message ?: "Failed to connect to SpringBoard Services"];
|
|
||||||
idevice_error_free(err);
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
void *pngData = NULL;
|
|
||||||
size_t dataLen = 0;
|
|
||||||
err = springboard_services_get_icon(client, [bundleID UTF8String], &pngData, &dataLen);
|
|
||||||
if (err) {
|
|
||||||
*error = [NSString stringWithUTF8String:err->message ?: "Failed to get app icon"];
|
|
||||||
idevice_error_free(err);
|
|
||||||
springboard_services_free(client);
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSData *data = [NSData dataWithBytes:pngData length:dataLen];
|
|
||||||
free(pngData);
|
|
||||||
UIImage *icon = [UIImage imageWithData:data];
|
|
||||||
|
|
||||||
springboard_services_free(client);
|
|
||||||
return icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
@implementation JITEnableContext(App)
|
|
||||||
|
|
||||||
- (NSDictionary<NSString*, NSString*>*)getAppListWithError:(NSError**)error {
|
|
||||||
[self ensureTunnelWithError:error];
|
|
||||||
if(*error) {
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSString* errorStr = nil;
|
|
||||||
NSDictionary<NSString*, NSString*>* apps = list_installed_apps(adapter, handshake, &errorStr);
|
|
||||||
if (errorStr) {
|
|
||||||
*error = [self errorWithStr:errorStr code:-17];
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
return apps;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSDictionary<NSString*, NSString*>*)getAllAppsWithError:(NSError**)error {
|
|
||||||
[self ensureTunnelWithError:error];
|
|
||||||
if(*error) {
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSString* errorStr = nil;
|
|
||||||
NSDictionary<NSString*, NSString*>* apps = list_all_apps(adapter, handshake, &errorStr);
|
|
||||||
if (errorStr) {
|
|
||||||
*error = [self errorWithStr:errorStr code:-17];
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
return apps;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSDictionary<NSString*, NSString*>*)getHiddenSystemAppsWithError:(NSError**)error {
|
|
||||||
[self ensureTunnelWithError:error];
|
|
||||||
if(*error) {
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSString* errorStr = nil;
|
|
||||||
NSDictionary<NSString*, NSString*>* apps = list_hidden_system_apps(adapter, handshake, &errorStr);
|
|
||||||
if (errorStr) {
|
|
||||||
*error = [self errorWithStr:errorStr code:-17];
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
return apps;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSArray<NSDictionary*>*)getSideloadedAppsWithError:(NSError**)error {
|
|
||||||
[self ensureTunnelWithError:error];
|
|
||||||
if(*error) {
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSString* errorStr = nil;
|
|
||||||
NSArray<NSDictionary*>* apps = getSideloadedApps(adapter, handshake, &errorStr);
|
|
||||||
if (errorStr) {
|
|
||||||
*error = [self errorWithStr:errorStr code:-17];
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
return apps;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (UIImage*)getAppIconWithBundleId:(NSString*)bundleId error:(NSError**)error {
|
|
||||||
[self ensureTunnelWithError:error];
|
|
||||||
if(*error) {
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSString* errorStr = nil;
|
|
||||||
UIImage* icon = getAppIcon(adapter, handshake, bundleId, &errorStr);
|
|
||||||
if (errorStr) {
|
|
||||||
*error = [self errorWithStr:errorStr code:-17];
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
return icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
//
|
|
||||||
// ideviceinfo.h
|
|
||||||
// StikDebug
|
|
||||||
//
|
|
||||||
// Created by Stephen on 8/2/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
#ifndef IDEVICEINFO_H
|
|
||||||
#define IDEVICEINFO_H
|
|
||||||
|
|
||||||
#include <stddef.h>
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endif // IDEVICEINFO_H
|
|
||||||
|
|
@ -1,66 +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"
|
|
||||||
#import "JITEnableContext.h"
|
|
||||||
#import "JITEnableContextInternal.h"
|
|
||||||
@import Foundation;
|
|
||||||
|
|
||||||
NSError* makeError(int code, NSString* msg);
|
|
||||||
LockdowndClientHandle* ideviceinfo_c_init(AdapterHandle* adapter, RsdHandshakeHandle* handshake, NSError** error) {
|
|
||||||
LockdowndClientHandle *g_client = NULL;
|
|
||||||
IdeviceFfiError *err = lockdownd_connect_rsd(adapter, handshake, &g_client);
|
|
||||||
if (err) {
|
|
||||||
*error = makeError(err->code, @(err->message));
|
|
||||||
idevice_error_free(err);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No session start needed - RSD tunnel handles authentication
|
|
||||||
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 ensureTunnelWithError:error];
|
|
||||||
if (*error) { return nil; }
|
|
||||||
return ideviceinfo_c_init(adapter, handshake, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (char*)ideviceInfoGetXMLWithLockdownClient:(LockdowndClientHandle*)lockdownClient error:(NSError**)error {
|
|
||||||
[self ensureTunnelWithError:error];
|
|
||||||
if (*error) { return NULL; }
|
|
||||||
return ideviceinfo_c_get_xml(lockdownClient, error);
|
|
||||||
}
|
|
||||||
@end
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
//
|
|
||||||
// jit.h
|
|
||||||
// StikJIT
|
|
||||||
//
|
|
||||||
// Created by Stephen on 3/27/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
// jit.h
|
|
||||||
#ifndef JIT_H
|
|
||||||
#define JIT_H
|
|
||||||
#include "idevice.h"
|
|
||||||
#include <dispatch/dispatch.h>
|
|
||||||
|
|
||||||
typedef void (^LogFuncC)(const char* message, ...);
|
|
||||||
typedef void (^DebugAppCallback)(int pid,
|
|
||||||
struct DebugProxyHandle* debug_proxy,
|
|
||||||
struct RemoteServerHandle* remote_server,
|
|
||||||
dispatch_semaphore_t semaphore);
|
|
||||||
int debug_app(AdapterHandle* adapter, RsdHandshakeHandle* handshake, const char *bundle_id, LogFuncC logger, DebugAppCallback callback);
|
|
||||||
int debug_app_pid(AdapterHandle* adapter, RsdHandshakeHandle* handshake, int pid, LogFuncC logger, DebugAppCallback callback);
|
|
||||||
int launch_app_via_proxy(AdapterHandle* adapter, RsdHandshakeHandle* handshake, const char *bundle_id, LogFuncC logger);
|
|
||||||
|
|
||||||
#endif /* JIT_H */
|
|
||||||
|
|
@ -1,219 +0,0 @@
|
||||||
//
|
|
||||||
// jit.c
|
|
||||||
// StikJIT
|
|
||||||
//
|
|
||||||
// Created by Stephen on 3/27/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
// Jackson Coxson
|
|
||||||
|
|
||||||
#include <arpa/inet.h>
|
|
||||||
#include <inttypes.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
#include <CoreFoundation/CoreFoundation.h>
|
|
||||||
#include <limits.h>
|
|
||||||
|
|
||||||
#include "jit.h"
|
|
||||||
#import "JITEnableContext.h"
|
|
||||||
#import "JITEnableContextInternal.h"
|
|
||||||
|
|
||||||
// MARK: - Shared debug session
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
RemoteServerHandle *remote_server;
|
|
||||||
DebugProxyHandle *debug_proxy;
|
|
||||||
} DebugSession;
|
|
||||||
|
|
||||||
static void debug_session_free(DebugSession *s) {
|
|
||||||
if (s->debug_proxy) { debug_proxy_free(s->debug_proxy); s->debug_proxy = NULL; }
|
|
||||||
if (s->remote_server) { remote_server_free(s->remote_server); s->remote_server = NULL; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connects to the device using the existing adapter+handshake and sets up the debug proxy.
|
|
||||||
// Returns 0 on success; cleans up any partial state and returns 1 on failure.
|
|
||||||
static int connect_debug_session(AdapterHandle *adapter, RsdHandshakeHandle *handshake, DebugSession *out) {
|
|
||||||
memset(out, 0, sizeof(*out));
|
|
||||||
IdeviceFfiError *err = NULL;
|
|
||||||
|
|
||||||
err = remote_server_connect_rsd(adapter, handshake, &out->remote_server);
|
|
||||||
if (err) { idevice_error_free(err); return 1; }
|
|
||||||
|
|
||||||
err = debug_proxy_connect_rsd(adapter, handshake, &out->debug_proxy);
|
|
||||||
if (err) { idevice_error_free(err); debug_session_free(out); return 1; }
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Debug server commands
|
|
||||||
|
|
||||||
void runDebugServerCommand(int pid,
|
|
||||||
DebugProxyHandle* debug_proxy,
|
|
||||||
RemoteServerHandle* remote_server,
|
|
||||||
LogFuncC logger,
|
|
||||||
DebugAppCallback callback) {
|
|
||||||
// Enable QStartNoAckMode
|
|
||||||
char *disableResponse = NULL;
|
|
||||||
debug_proxy_send_ack(debug_proxy);
|
|
||||||
debug_proxy_send_ack(debug_proxy);
|
|
||||||
DebugserverCommandHandle *disableAckCommand = debugserver_command_new("QStartNoAckMode", NULL, 0);
|
|
||||||
IdeviceFfiError* err = debug_proxy_send_command(debug_proxy, disableAckCommand, &disableResponse);
|
|
||||||
debugserver_command_free(disableAckCommand);
|
|
||||||
logger("QStartNoAckMode result = %s, err = %d", disableResponse, err);
|
|
||||||
idevice_string_free(disableResponse);
|
|
||||||
debug_proxy_set_ack_mode(debug_proxy, false);
|
|
||||||
|
|
||||||
if (callback) {
|
|
||||||
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
|
|
||||||
callback(pid, debug_proxy, remote_server, semaphore);
|
|
||||||
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
|
|
||||||
err = debug_proxy_send_raw(debug_proxy, "\x03", 1);
|
|
||||||
usleep(500);
|
|
||||||
} else {
|
|
||||||
char attach_command[64];
|
|
||||||
snprintf(attach_command, sizeof(attach_command), "vAttach;%" PRIx64, pid);
|
|
||||||
|
|
||||||
DebugserverCommandHandle *attach_cmd = debugserver_command_new(attach_command, NULL, 0);
|
|
||||||
if (attach_cmd == NULL) {
|
|
||||||
logger("Failed to create attach command");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
char *attach_response = NULL;
|
|
||||||
err = debug_proxy_send_command(debug_proxy, attach_cmd, &attach_response);
|
|
||||||
debugserver_command_free(attach_cmd);
|
|
||||||
if (err) {
|
|
||||||
logger("Failed to attach to process: %d", err->code);
|
|
||||||
idevice_error_free(err);
|
|
||||||
} else if (attach_response != NULL) {
|
|
||||||
logger("Attach response: %s", attach_response);
|
|
||||||
idevice_string_free(attach_response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send detach command
|
|
||||||
DebugserverCommandHandle *detach_cmd = debugserver_command_new("D", NULL, 0);
|
|
||||||
if (detach_cmd == NULL) {
|
|
||||||
logger("Failed to create detach command");
|
|
||||||
} else {
|
|
||||||
char *detach_response = NULL;
|
|
||||||
err = debug_proxy_send_command(debug_proxy, detach_cmd, &detach_response);
|
|
||||||
debugserver_command_free(detach_cmd);
|
|
||||||
if (err) {
|
|
||||||
logger("Failed to detach from process: %d", err->code);
|
|
||||||
idevice_error_free(err);
|
|
||||||
} else if (detach_response != NULL) {
|
|
||||||
logger("Detach response: %s", detach_response);
|
|
||||||
idevice_string_free(detach_response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Public entry points
|
|
||||||
|
|
||||||
int debug_app(AdapterHandle* adapter, RsdHandshakeHandle* handshake, const char *bundle_id, LogFuncC logger, DebugAppCallback callback) {
|
|
||||||
DebugSession session;
|
|
||||||
if (connect_debug_session(adapter, handshake, &session) != 0) return 1;
|
|
||||||
|
|
||||||
ProcessControlHandle *process_control = NULL;
|
|
||||||
IdeviceFfiError *err = process_control_new(session.remote_server, &process_control);
|
|
||||||
if (err) {
|
|
||||||
idevice_error_free(err);
|
|
||||||
debug_session_free(&session);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint64_t pid = 0;
|
|
||||||
err = process_control_launch_app(process_control, bundle_id, NULL, 0, NULL, 0, true, false, &pid);
|
|
||||||
if (err) {
|
|
||||||
idevice_error_free(err);
|
|
||||||
process_control_free(process_control);
|
|
||||||
debug_session_free(&session);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
runDebugServerCommand((int)pid, session.debug_proxy, session.remote_server, logger, callback);
|
|
||||||
|
|
||||||
process_control_free(process_control);
|
|
||||||
debug_session_free(&session);
|
|
||||||
logger("Debug session completed");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int debug_app_pid(AdapterHandle* adapter, RsdHandshakeHandle* handshake, int pid, LogFuncC logger, DebugAppCallback callback) {
|
|
||||||
DebugSession session;
|
|
||||||
if (connect_debug_session(adapter, handshake, &session) != 0) return 1;
|
|
||||||
|
|
||||||
runDebugServerCommand(pid, session.debug_proxy, session.remote_server, logger, callback);
|
|
||||||
|
|
||||||
debug_session_free(&session);
|
|
||||||
logger("Debug session completed");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int launch_app_via_proxy(AdapterHandle* adapter, RsdHandshakeHandle* handshake, const char *bundle_id, LogFuncC logger) {
|
|
||||||
IdeviceFfiError* err = NULL;
|
|
||||||
|
|
||||||
RemoteServerHandle *remote_server = NULL;
|
|
||||||
ProcessControlHandle *process_control = NULL;
|
|
||||||
uint64_t pid = 0;
|
|
||||||
int result = 1;
|
|
||||||
|
|
||||||
err = remote_server_connect_rsd(adapter, handshake, &remote_server);
|
|
||||||
if (err) { idevice_error_free(err); goto cleanup; }
|
|
||||||
|
|
||||||
err = process_control_new(remote_server, &process_control);
|
|
||||||
if (err) { idevice_error_free(err); goto cleanup; }
|
|
||||||
|
|
||||||
err = process_control_launch_app(process_control, bundle_id, NULL, 0, NULL, 0, false, true, &pid);
|
|
||||||
if (err) {
|
|
||||||
idevice_error_free(err);
|
|
||||||
if (logger) logger("Failed to launch app: %s", bundle_id);
|
|
||||||
goto cleanup;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (logger) logger("Launched app (PID %llu)", pid);
|
|
||||||
result = 0;
|
|
||||||
|
|
||||||
cleanup:
|
|
||||||
if (process_control) process_control_free(process_control);
|
|
||||||
if (remote_server) remote_server_free(remote_server);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@implementation JITEnableContext(JIT)
|
|
||||||
|
|
||||||
- (BOOL)debugAppWithBundleID:(NSString*)bundleID logger:(LogFunc)logger jsCallback:(DebugAppCallback)jsCallback {
|
|
||||||
NSError* err = nil;
|
|
||||||
[self ensureTunnelWithError:&err];
|
|
||||||
if (err) {
|
|
||||||
logger(err.localizedDescription);
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
return debug_app(adapter, handshake, [bundleID UTF8String], [self createCLogger:logger], jsCallback) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)debugAppWithPID:(int)pid logger:(LogFunc)logger jsCallback:(DebugAppCallback)jsCallback {
|
|
||||||
NSError* err = nil;
|
|
||||||
[self ensureTunnelWithError:&err];
|
|
||||||
if (err) {
|
|
||||||
logger(err.localizedDescription);
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
return debug_app_pid(adapter, handshake, pid, [self createCLogger:logger], jsCallback) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)launchAppWithoutDebug:(NSString*)bundleID logger:(LogFunc)logger {
|
|
||||||
NSError* err = nil;
|
|
||||||
[self ensureTunnelWithError:&err];
|
|
||||||
if (err) {
|
|
||||||
logger(err.localizedDescription);
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
return launch_app_via_proxy(adapter, handshake, [bundleID UTF8String], [self createCLogger:logger]) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
@ -1,111 +0,0 @@
|
||||||
//
|
|
||||||
// location_simulation.c
|
|
||||||
// StikDebug
|
|
||||||
//
|
|
||||||
// Created by Stephen on 8/3/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
#include "location_simulation.h"
|
|
||||||
#include "idevice.h"
|
|
||||||
#include <arpa/inet.h>
|
|
||||||
#include <netinet/in.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
|
|
||||||
static AdapterHandle *g_adapter = NULL;
|
|
||||||
static RsdHandshakeHandle *g_handshake = NULL;
|
|
||||||
static RemoteServerHandle *g_remote_server = NULL;
|
|
||||||
static LocationSimulationHandle *g_location_sim = NULL;
|
|
||||||
|
|
||||||
static void cleanup_on_error(void) {
|
|
||||||
if (g_location_sim) { location_simulation_free(g_location_sim); g_location_sim = NULL; }
|
|
||||||
if (g_remote_server) { remote_server_free(g_remote_server); g_remote_server = NULL; }
|
|
||||||
if (g_handshake) { rsd_handshake_free(g_handshake); g_handshake = NULL; }
|
|
||||||
if (g_adapter) { adapter_free(g_adapter); g_adapter = NULL; }
|
|
||||||
}
|
|
||||||
|
|
||||||
int simulate_location(const char *device_ip,
|
|
||||||
double latitude,
|
|
||||||
double longitude,
|
|
||||||
const char *pairing_file)
|
|
||||||
{
|
|
||||||
IdeviceFfiError *err = NULL;
|
|
||||||
|
|
||||||
if (g_location_sim) {
|
|
||||||
if ((err = location_simulation_set(g_location_sim, latitude, longitude))) {
|
|
||||||
idevice_error_free(err);
|
|
||||||
cleanup_on_error();
|
|
||||||
} else {
|
|
||||||
return IPA_OK;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct sockaddr_in addr = { .sin_family = AF_INET,
|
|
||||||
.sin_port = htons(49152) };
|
|
||||||
if (inet_pton(AF_INET, device_ip, &addr.sin_addr) != 1) {
|
|
||||||
return IPA_ERR_INVALID_IP;
|
|
||||||
}
|
|
||||||
|
|
||||||
RpPairingFileHandle *rp_pairing = NULL;
|
|
||||||
if ((err = rp_pairing_file_read(pairing_file, &rp_pairing))) {
|
|
||||||
idevice_error_free(err);
|
|
||||||
return IPA_ERR_PAIRING_READ;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((err = tunnel_create_rppairing(
|
|
||||||
(const idevice_sockaddr *)&addr,
|
|
||||||
sizeof(addr),
|
|
||||||
"StikDebugLocation",
|
|
||||||
rp_pairing,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
&g_adapter,
|
|
||||||
&g_handshake)))
|
|
||||||
{
|
|
||||||
idevice_error_free(err);
|
|
||||||
rp_pairing_file_free(rp_pairing);
|
|
||||||
cleanup_on_error();
|
|
||||||
return IPA_ERR_PROVIDER_CREATE;
|
|
||||||
}
|
|
||||||
rp_pairing_file_free(rp_pairing);
|
|
||||||
|
|
||||||
if ((err = remote_server_connect_rsd(g_adapter,
|
|
||||||
g_handshake,
|
|
||||||
&g_remote_server)))
|
|
||||||
{
|
|
||||||
idevice_error_free(err);
|
|
||||||
cleanup_on_error();
|
|
||||||
return IPA_ERR_REMOTE_SERVER;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((err = location_simulation_new(g_remote_server,
|
|
||||||
&g_location_sim))) {
|
|
||||||
idevice_error_free(err);
|
|
||||||
cleanup_on_error();
|
|
||||||
return IPA_ERR_LOCATION_SIM;
|
|
||||||
}
|
|
||||||
// location_simulation_new takes ownership of g_remote_server.
|
|
||||||
g_remote_server = NULL;
|
|
||||||
|
|
||||||
if ((err = location_simulation_set(g_location_sim,
|
|
||||||
latitude,
|
|
||||||
longitude))) {
|
|
||||||
idevice_error_free(err);
|
|
||||||
cleanup_on_error();
|
|
||||||
return IPA_ERR_LOCATION_SET;
|
|
||||||
}
|
|
||||||
|
|
||||||
return IPA_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
int clear_simulated_location(void)
|
|
||||||
{
|
|
||||||
IdeviceFfiError *err = NULL;
|
|
||||||
if (!g_location_sim) return IPA_ERR_LOCATION_CLEAR;
|
|
||||||
|
|
||||||
err = location_simulation_clear(g_location_sim);
|
|
||||||
cleanup_on_error();
|
|
||||||
|
|
||||||
return err ? IPA_ERR_LOCATION_CLEAR : IPA_OK;
|
|
||||||
}
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
//
|
|
||||||
// location_simulation.h
|
|
||||||
// StikDebug
|
|
||||||
//
|
|
||||||
// Created by Stephen on 8/3/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
#ifndef LOCATION_SIMULATION_H
|
|
||||||
#define LOCATION_SIMULATION_H
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Success / error codes
|
|
||||||
#define IPA_OK 0
|
|
||||||
#define IPA_ERR_INVALID_IP 1
|
|
||||||
#define IPA_ERR_PAIRING_READ 2
|
|
||||||
#define IPA_ERR_PROVIDER_CREATE 3
|
|
||||||
#define IPA_ERR_CORE_DEVICE 4
|
|
||||||
#define IPA_ERR_RSD_PORT 5
|
|
||||||
#define IPA_ERR_ADAPTER_CREATE 6
|
|
||||||
#define IPA_ERR_STREAM 7
|
|
||||||
#define IPA_ERR_HANDSHAKE 8
|
|
||||||
#define IPA_ERR_REMOTE_SERVER 9
|
|
||||||
#define IPA_ERR_LOCATION_SIM 10
|
|
||||||
#define IPA_ERR_LOCATION_SET 11
|
|
||||||
#define IPA_ERR_LOCATION_CLEAR 12
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simulate the device’s location.
|
|
||||||
* @param device_ip IP address of the device.
|
|
||||||
* @param latitude Latitude to simulate.
|
|
||||||
* @param longitude Longitude to simulate.
|
|
||||||
* @param pairing_file Path to the pairing file.
|
|
||||||
* @return IPA_OK (0) on success, or one of the IPA_ERR_* codes.
|
|
||||||
*/
|
|
||||||
int simulate_location(const char *device_ip,
|
|
||||||
double latitude,
|
|
||||||
double longitude,
|
|
||||||
const char *pairing_file);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear any simulated location.
|
|
||||||
* @return IPA_OK (0) on success, or IPA_ERR_LOCATION_CLEAR.
|
|
||||||
*/
|
|
||||||
int clear_simulated_location(void);
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endif /* LOCATION_SIMULATION_H */
|
|
||||||
4
StikJIT/idevice/module.modulemap
Normal file
4
StikJIT/idevice/module.modulemap
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
module idevice [system] {
|
||||||
|
header "idevice.h"
|
||||||
|
export *
|
||||||
|
}
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
//
|
|
||||||
// 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(AdapterHandle* adapter, RsdHandshakeHandle* handshake, NSError** error);
|
|
||||||
int mountPersonalDDI(AdapterHandle* adapter, RsdHandshakeHandle* handshake, NSString* imagePath, NSString* trustcachePath, NSString* manifestPath, NSError** error);
|
|
||||||
#endif
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
//
|
|
||||||
// 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(AdapterHandle* adapter, RsdHandshakeHandle* handshake, NSError** error) {
|
|
||||||
ImageMounterHandle *client = NULL;
|
|
||||||
IdeviceFfiError *err = image_mounter_connect_rsd(adapter, handshake, &client);
|
|
||||||
if (err) {
|
|
||||||
*error = makeError(err->code, @(err->message));
|
|
||||||
idevice_error_free(err);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
plist_t *devices = NULL;
|
|
||||||
size_t deviceLength = 0;
|
|
||||||
err = image_mounter_copy_devices(client, &devices, &deviceLength);
|
|
||||||
image_mounter_free(client);
|
|
||||||
if (err) {
|
|
||||||
*error = makeError(err->code, @(err->message));
|
|
||||||
idevice_error_free(err);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < (int)deviceLength; i++) {
|
|
||||||
plist_free(devices[i]);
|
|
||||||
}
|
|
||||||
idevice_data_free((uint8_t *)devices, deviceLength * sizeof(plist_t));
|
|
||||||
return deviceLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
int mountPersonalDDI(AdapterHandle* adapter, RsdHandshakeHandle* handshake, NSString* imagePath, NSString* trustcachePath, NSString* manifestPath, NSError** error) {
|
|
||||||
NSData *image = [NSData dataWithContentsOfFile:imagePath];
|
|
||||||
NSData *trustcache = [NSData dataWithContentsOfFile:trustcachePath];
|
|
||||||
NSData *buildManifest = [NSData dataWithContentsOfFile:manifestPath];
|
|
||||||
if (!image || !trustcache || !buildManifest) {
|
|
||||||
*error = makeError(1, @"Failed to read one or more files");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get UniqueChipID via lockdownd over RSD (no session start needed - tunnel handles auth)
|
|
||||||
LockdowndClientHandle *lockdownClient = NULL;
|
|
||||||
IdeviceFfiError *err = lockdownd_connect_rsd(adapter, handshake, &lockdownClient);
|
|
||||||
if (err) {
|
|
||||||
*error = makeError(6, @(err->message));
|
|
||||||
idevice_error_free(err);
|
|
||||||
return 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
plist_t uniqueChipIDPlist = NULL;
|
|
||||||
err = lockdownd_get_value(lockdownClient, "UniqueChipID", NULL, &uniqueChipIDPlist);
|
|
||||||
lockdownd_client_free(lockdownClient);
|
|
||||||
if (err) {
|
|
||||||
*error = makeError(8, @(err->message));
|
|
||||||
idevice_error_free(err);
|
|
||||||
return 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint64_t uniqueChipID = 0;
|
|
||||||
plist_get_uint_val(uniqueChipIDPlist, &uniqueChipID);
|
|
||||||
plist_free(uniqueChipIDPlist);
|
|
||||||
|
|
||||||
ImageMounterHandle *mounterClient = NULL;
|
|
||||||
err = image_mounter_connect_rsd(adapter, handshake, &mounterClient);
|
|
||||||
if (err) {
|
|
||||||
*error = makeError(9, @(err->message));
|
|
||||||
idevice_error_free(err);
|
|
||||||
return 9;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: image_mounter_mount_personalized still requires an IdeviceProviderHandle.
|
|
||||||
// The FFI needs an RSD variant (e.g. image_mounter_mount_personalized_rsd) that
|
|
||||||
// takes an AdapterHandle instead.
|
|
||||||
*error = makeError(10, @"mount_personalized not yet available over RSD tunnels");
|
|
||||||
image_mounter_free(mounterClient);
|
|
||||||
return 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
@implementation JITEnableContext(DDI)
|
|
||||||
|
|
||||||
- (NSUInteger)getMountedDeviceCount:(NSError**)error {
|
|
||||||
[self ensureTunnelWithError:error];
|
|
||||||
if (*error) { return 0; }
|
|
||||||
return getMountedDeviceCount(adapter, handshake, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSInteger)mountPersonalDDIWithImagePath:(NSString*)imagePath trustcachePath:(NSString*)trustcachePath manifestPath:(NSString*)manifestPath error:(NSError**)error {
|
|
||||||
[self ensureTunnelWithError:error];
|
|
||||||
if (*error) { return 0; }
|
|
||||||
return mountPersonalDDI(adapter, handshake, imagePath, trustcachePath, manifestPath, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
@ -1,130 +0,0 @@
|
||||||
//
|
|
||||||
// process.m
|
|
||||||
// StikDebug
|
|
||||||
//
|
|
||||||
// Created by s s on 2025/12/12.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "JITEnableContext.h"
|
|
||||||
#import "JITEnableContextInternal.h"
|
|
||||||
@import Foundation;
|
|
||||||
|
|
||||||
// MARK: - Shared AppService session
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
AppServiceHandle *appService;
|
|
||||||
} AppServiceSession;
|
|
||||||
|
|
||||||
static void app_service_session_free(AppServiceSession *s) {
|
|
||||||
if (s->appService) { app_service_free(s->appService); s->appService = NULL; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connects to the device via the existing adapter+handshake → AppService.
|
|
||||||
// Returns 0 on success; cleans up any partial state and returns 1 on failure.
|
|
||||||
static int connect_app_service(AdapterHandle *adapter,
|
|
||||||
RsdHandshakeHandle *handshake,
|
|
||||||
AppServiceSession *out,
|
|
||||||
JITEnableContext *ctx,
|
|
||||||
NSError **outError)
|
|
||||||
{
|
|
||||||
memset(out, 0, sizeof(*out));
|
|
||||||
IdeviceFfiError *ffiError = NULL;
|
|
||||||
|
|
||||||
ffiError = app_service_connect_rsd(adapter, handshake, &out->appService);
|
|
||||||
if (ffiError) {
|
|
||||||
*outError = [ctx errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "Unable to open AppService"]
|
|
||||||
code:ffiError->code];
|
|
||||||
idevice_error_free(ffiError);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - JITEnableContext(Process)
|
|
||||||
|
|
||||||
@implementation JITEnableContext(Process)
|
|
||||||
|
|
||||||
- (NSArray<NSDictionary*>*)fetchProcessesViaAppServiceWithError:(NSError **)error {
|
|
||||||
[self ensureTunnelWithError:error];
|
|
||||||
if (*error) { return nil; }
|
|
||||||
|
|
||||||
AppServiceSession session;
|
|
||||||
if (connect_app_service(adapter, handshake, &session, self, error) != 0) { return nil; }
|
|
||||||
|
|
||||||
ProcessTokenC *processes = NULL;
|
|
||||||
uintptr_t count = 0;
|
|
||||||
IdeviceFfiError *ffiError = app_service_list_processes(session.appService, &processes, &count);
|
|
||||||
|
|
||||||
NSMutableArray *result = nil;
|
|
||||||
if (ffiError) {
|
|
||||||
if (error) {
|
|
||||||
*error = [self errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "Failed to list processes"]
|
|
||||||
code:ffiError->code];
|
|
||||||
}
|
|
||||||
idevice_error_free(ffiError);
|
|
||||||
} else {
|
|
||||||
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];
|
|
||||||
}
|
|
||||||
if (processes && count > 0) {
|
|
||||||
app_service_free_process_list(processes, count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
app_service_session_free(&session);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSArray<NSDictionary*>*)_fetchProcessListLocked:(NSError**)error {
|
|
||||||
[self ensureTunnelWithError: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 ensureTunnelWithError:error];
|
|
||||||
if (*error) { return NO; }
|
|
||||||
|
|
||||||
AppServiceSession session;
|
|
||||||
if (connect_app_service(adapter, handshake, &session, self, error) != 0) { return NO; }
|
|
||||||
|
|
||||||
SignalResponseC *signalResponse = NULL;
|
|
||||||
IdeviceFfiError *ffiError = app_service_send_signal(session.appService, (uint32_t)pid, SIGKILL, &signalResponse);
|
|
||||||
|
|
||||||
BOOL success = NO;
|
|
||||||
if (ffiError) {
|
|
||||||
if (error) {
|
|
||||||
*error = [self errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "Failed to kill process"]
|
|
||||||
code:ffiError->code];
|
|
||||||
}
|
|
||||||
idevice_error_free(ffiError);
|
|
||||||
} else {
|
|
||||||
success = YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (signalResponse) { app_service_free_signal_response(signalResponse); }
|
|
||||||
app_service_session_free(&session);
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
//
|
|
||||||
// profiles.h
|
|
||||||
// StikDebug
|
|
||||||
//
|
|
||||||
// Created by s s on 2025/11/29.
|
|
||||||
//
|
|
||||||
|
|
||||||
#ifndef PROFILES_H
|
|
||||||
#define PROFILES_H
|
|
||||||
#include "idevice.h"
|
|
||||||
#include <Foundation/Foundation.h>
|
|
||||||
NSArray<NSData*>* _Nullable fetchAppProfiles(AdapterHandle* _Nonnull adapter, RsdHandshakeHandle* _Nonnull handshake, NSError* _Nullable * _Nullable error);
|
|
||||||
bool removeProfile(AdapterHandle* _Nonnull adapter, RsdHandshakeHandle* _Nonnull handshake, NSString* _Nonnull uuid, NSError* _Nullable * _Nullable error);
|
|
||||||
bool addProfile(AdapterHandle* _Nonnull adapter, RsdHandshakeHandle* _Nonnull handshake, NSData* _Nonnull profile, NSError* _Nullable * _Nullable error);
|
|
||||||
|
|
||||||
@interface CMSDecoderHelper : NSObject
|
|
||||||
// Decode CMS/PKCS7 data and return decoded payload and any embedded certs
|
|
||||||
+ (NSData*)decodeCMSData:(NSData *)cmsData
|
|
||||||
// outCerts:(NSArray<id> * _Nullable * _Nullable)outCerts
|
|
||||||
error:(NSError * _Nullable * _Nullable)error;
|
|
||||||
@end
|
|
||||||
#endif
|
|
||||||
|
|
@ -1,170 +0,0 @@
|
||||||
//
|
|
||||||
// profiles.m
|
|
||||||
// StikDebug
|
|
||||||
//
|
|
||||||
// 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) {
|
|
||||||
return [NSError errorWithDomain:@"profiles" code:code userInfo:@{NSLocalizedDescriptionKey: msg}];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
NSArray<NSData*>* fetchAppProfiles(AdapterHandle* adapter, RsdHandshakeHandle* handshake, NSError** error) {
|
|
||||||
MisagentClientHandle *misagentHandle = NULL;
|
|
||||||
IdeviceFfiError *err = misagent_connect_rsd(adapter, handshake, &misagentHandle);
|
|
||||||
if (err) {
|
|
||||||
*error = makeError(err->code, @(err->message));
|
|
||||||
idevice_error_free(err);
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t **profileArr = NULL;
|
|
||||||
size_t profileCount = 0;
|
|
||||||
size_t *profileLengthArr = NULL;
|
|
||||||
err = misagent_copy_all(misagentHandle, &profileArr, &profileLengthArr, &profileCount);
|
|
||||||
|
|
||||||
if (err) {
|
|
||||||
*error = makeError((err)->code, @((err)->message));
|
|
||||||
misagent_client_free(misagentHandle);
|
|
||||||
idevice_error_free(err);
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSMutableArray* ans = [NSMutableArray array];
|
|
||||||
for(int i = 0; i < profileCount; ++i) {
|
|
||||||
size_t len = profileLengthArr[i];
|
|
||||||
uint8_t* profile = profileArr[i];
|
|
||||||
NSData* profileData = [NSData dataWithBytes:profile length:len];
|
|
||||||
|
|
||||||
[ans addObject:profileData];
|
|
||||||
}
|
|
||||||
|
|
||||||
misagent_free_profiles(profileArr, profileLengthArr, profileCount);
|
|
||||||
misagent_client_free(misagentHandle);
|
|
||||||
|
|
||||||
return ans;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool removeProfile(AdapterHandle* adapter, RsdHandshakeHandle* handshake, NSString* uuid, NSError** error) {
|
|
||||||
MisagentClientHandle *misagentHandle = NULL;
|
|
||||||
IdeviceFfiError * err = misagent_connect_rsd(adapter, handshake, &misagentHandle);
|
|
||||||
if (err) {
|
|
||||||
*error = makeError(err->code, @(err->message));
|
|
||||||
idevice_error_free(err);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
err = misagent_remove(misagentHandle, [uuid UTF8String]);
|
|
||||||
if (err) {
|
|
||||||
*error = makeError((err)->code, @((err)->message));
|
|
||||||
misagent_client_free(misagentHandle);
|
|
||||||
idevice_error_free(err);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
misagent_client_free(misagentHandle);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool addProfile(AdapterHandle* adapter, RsdHandshakeHandle* handshake, NSData* profile, NSError** error) {
|
|
||||||
MisagentClientHandle *misagentHandle = NULL;
|
|
||||||
IdeviceFfiError * err = misagent_connect_rsd(adapter, handshake, &misagentHandle);
|
|
||||||
if (err) {
|
|
||||||
*error = makeError(err->code, @(err->message));
|
|
||||||
idevice_error_free(err);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
err = misagent_install(misagentHandle, [profile bytes], [profile length]);
|
|
||||||
if (err) {
|
|
||||||
*error = makeError((err)->code, @((err)->message));
|
|
||||||
misagent_client_free(misagentHandle);
|
|
||||||
idevice_error_free(err);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
misagent_client_free(misagentHandle);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@implementation CMSDecoderHelper
|
|
||||||
|
|
||||||
+ (NSData*)decodeCMSData:(NSData *)cmsData
|
|
||||||
// outCerts:(NSArray<id> * _Nullable * _Nullable)outCerts
|
|
||||||
error:(NSError * _Nullable * _Nullable)error
|
|
||||||
{
|
|
||||||
if (!cmsData || cmsData.length == 0) {
|
|
||||||
if (error) {
|
|
||||||
*error = [NSError errorWithDomain:NSCocoaErrorDomain
|
|
||||||
code:NSURLErrorBadURL
|
|
||||||
userInfo:@{NSLocalizedDescriptionKey: @"Invalid or empty CMS payload"}];
|
|
||||||
}
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSData *xmlStart = [@"<?xml" dataUsingEncoding:NSASCIIStringEncoding];
|
|
||||||
NSData *plistEnd = [@"</plist>" dataUsingEncoding:NSASCIIStringEncoding];
|
|
||||||
NSData *binaryMagic = [@"bplist00" dataUsingEncoding:NSASCIIStringEncoding];
|
|
||||||
|
|
||||||
NSRange startRange = [cmsData rangeOfData:xmlStart options:0 range:NSMakeRange(0, cmsData.length)];
|
|
||||||
if (startRange.location != NSNotFound) {
|
|
||||||
NSRange endSearchRange = NSMakeRange(startRange.location, cmsData.length - startRange.location);
|
|
||||||
NSRange endRange = [cmsData rangeOfData:plistEnd options:0 range:endSearchRange];
|
|
||||||
if (endRange.location != NSNotFound) {
|
|
||||||
NSUInteger plistEndIndex = NSMaxRange(endRange);
|
|
||||||
if (plistEndIndex > startRange.location && plistEndIndex <= cmsData.length) {
|
|
||||||
return [cmsData subdataWithRange:NSMakeRange(startRange.location, plistEndIndex - startRange.location)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NSRange binaryRange = [cmsData rangeOfData:binaryMagic options:0 range:NSMakeRange(0, cmsData.length)];
|
|
||||||
if (binaryRange.location != NSNotFound) {
|
|
||||||
return [cmsData subdataWithRange:NSMakeRange(binaryRange.location, cmsData.length - binaryRange.location)];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
*error = [NSError errorWithDomain:NSCocoaErrorDomain
|
|
||||||
code:NSFileReadUnknownError
|
|
||||||
userInfo:@{NSLocalizedDescriptionKey: @"Unable to extract plist from CMS payload"}];
|
|
||||||
}
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation JITEnableContext(Profile)
|
|
||||||
|
|
||||||
- (NSArray<NSData*>*)fetchAllProfiles:(NSError **)error {
|
|
||||||
[self ensureTunnelWithError:error];
|
|
||||||
if(*error) {
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fetchAppProfiles(adapter, handshake, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)removeProfileWithUUID:(NSString*)uuid error:(NSError **)error {
|
|
||||||
[self ensureTunnelWithError:error];
|
|
||||||
if(*error) {
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
return removeProfile(adapter, handshake, uuid, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)addProfile:(NSData*)profile error:(NSError **)error {
|
|
||||||
[self ensureTunnelWithError:error];
|
|
||||||
if(*error) {
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
return addProfile(adapter, handshake, profile, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
@ -1,125 +0,0 @@
|
||||||
//
|
|
||||||
// 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 ensureTunnelWithError:&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_rsd(strongSelf->adapter, strongSelf->handshake, &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
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue