mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-16 20:30:30 +00:00
Merge branch 'resolve-conflicts' into main
This commit is contained in:
commit
ae4afaff80
8 changed files with 341 additions and 31 deletions
67
.github/workflows/build_ipa.yml
vendored
Normal file
67
.github/workflows/build_ipa.yml
vendored
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
name: Build Unsigned IPA
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Unsigned IPA
|
||||
runs-on: macos-14
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: 'latest'
|
||||
|
||||
- name: Fix Project Format
|
||||
run: |
|
||||
sed -i '' 's/objectVersion = 77/objectVersion = 56/' StikJIT.xcodeproj/project.pbxproj
|
||||
sed -i '' 's/preferredProjectObjectVersion = 77/preferredProjectObjectVersion = 56/' StikJIT.xcodeproj/project.pbxproj
|
||||
|
||||
- name: List Available SDKs
|
||||
run: xcodebuild -showsdks
|
||||
|
||||
- name: List Available Schemes
|
||||
run: xcodebuild -list -project StikJit.xcodeproj
|
||||
|
||||
- name: Archive Build (Unsigned)
|
||||
run: |
|
||||
xcodebuild clean archive \
|
||||
-project StikJIT.xcodeproj \
|
||||
-scheme "StikJIT" \
|
||||
-configuration Release \
|
||||
-archivePath build/StikJit.xcarchive \
|
||||
-sdk iphoneos \
|
||||
-destination 'generic/platform=iOS' \
|
||||
ONLY_ACTIVE_ARCH=NO \
|
||||
CODE_SIGN_IDENTITY="" \
|
||||
CODE_SIGNING_REQUIRED=NO \
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
SWIFT_OPTIMIZATION_LEVEL="-Onone" \
|
||||
IPHONEOS_DEPLOYMENT_TARGET=17.4
|
||||
|
||||
- name: Create IPA from Archive
|
||||
run: |
|
||||
# Copy the .app from the archive
|
||||
cp -R build/StikJit.xcarchive/Products/Applications/StikJit.app .
|
||||
|
||||
# Package the app into an IPA
|
||||
mkdir -p Payload
|
||||
cp -R StikJit.app Payload/
|
||||
zip -r StikJit.ipa Payload
|
||||
rm -rf Payload StikJit.app
|
||||
|
||||
- name: Upload IPA
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: StikJit.ipa
|
||||
path: StikJit.ipa
|
||||
retention-days: 90
|
||||
|
|
@ -7,24 +7,77 @@
|
|||
|
||||
import SwiftUI
|
||||
import em_proxy
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
@main
|
||||
struct HeartbeatApp: App {
|
||||
@State private var isLoading = true
|
||||
@State private var isPairing = false
|
||||
@State private var heartBeat = false
|
||||
@State private var error: Int32? = nil
|
||||
|
||||
init() {
|
||||
let fixMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, #selector(UIDocumentPickerViewController.fix_init(forOpeningContentTypes:asCopy:)))!
|
||||
let origMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, #selector(UIDocumentPickerViewController.init(forOpeningContentTypes:asCopy:)))!
|
||||
method_exchangeImplementations(origMethod, fixMethod)
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
if isLoading {
|
||||
LoadingView()
|
||||
.onAppear {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
|
||||
withAnimation(.easeInOut(duration: 1.0)) {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
startProxy()
|
||||
if FileManager.default.fileExists(atPath: URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path) {
|
||||
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
|
||||
if heartBeat {
|
||||
isLoading = false
|
||||
timer.invalidate()
|
||||
} else {
|
||||
if let error {
|
||||
if error == InvalidHostID.rawValue {
|
||||
isPairing = true
|
||||
} else {
|
||||
startHeartbeatInBackground()
|
||||
}
|
||||
self.error = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startHeartbeatInBackground()
|
||||
} else {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
.fileImporter(isPresented: $isPairing, allowedContentTypes: [UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!, .propertyList]) {result in
|
||||
switch result {
|
||||
|
||||
case .success(let url):
|
||||
let fileManager = FileManager.default
|
||||
let accessing = url.startAccessingSecurityScopedResource()
|
||||
|
||||
if fileManager.fileExists(atPath: url.path) {
|
||||
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"))
|
||||
print("File copied successfully!")
|
||||
startHeartbeatInBackground()
|
||||
} catch {
|
||||
print("Error copying file: \(error)")
|
||||
}
|
||||
} else {
|
||||
print("Source file does not exist.")
|
||||
}
|
||||
|
||||
if accessing {
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
case .failure(_):
|
||||
print("Failed")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -50,16 +103,57 @@ struct HeartbeatApp: App {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func startHeartbeatInBackground() {
|
||||
let heartBeat = Thread {
|
||||
let cCompletionHandler: @convention(block) (Int32, UnsafePointer<CChar>?) -> Void = { result, messagePointer in
|
||||
let message: String? = messagePointer != nil ? String(cString: messagePointer!) : nil
|
||||
|
||||
if result == 0 {
|
||||
print("Heartbeat started successfully: \(message ?? "")")
|
||||
|
||||
self.heartBeat = true
|
||||
} else {
|
||||
print("Error: \(result == InvalidHostID.rawValue ? "Invalid host ID, Please Selecr New Pairing File" : message ?? "") (Code: \(result))")
|
||||
|
||||
showAlert(title: "HeartBeat Error", message: "\(message ?? "") (\(result))", showOk: true) { _ in
|
||||
self.error = result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startHeartbeat(cCompletionHandler)
|
||||
}
|
||||
|
||||
heartBeat.qualityOfService = .background
|
||||
heartBeat.name = "HeartBeat"
|
||||
heartBeat.start()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
func startHeartbeatInBackground() {
|
||||
let heartBeat = Thread {
|
||||
startHeartbeat()
|
||||
let cCompletionHandler: @convention(block) (Int32, UnsafePointer<CChar>?) -> Void = { result, messagePointer in
|
||||
let message: String? = messagePointer != nil ? String(cString: messagePointer!) : nil
|
||||
|
||||
if result == 0 {
|
||||
print("Heartbeat started successfully: \(message ?? "")")
|
||||
} else {
|
||||
print("Error: \(message ?? "") (Code: \(result))")
|
||||
|
||||
showAlert(title: "HeartBeat Error", message: "\(message ?? "") (\(result))", showOk: true) { _ in
|
||||
startHeartbeatInBackground()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startHeartbeat(cCompletionHandler)
|
||||
}
|
||||
|
||||
heartBeat.qualityOfService = .background
|
||||
heartBeat.name = "HeartBeat"
|
||||
heartBeat.name = "Heartbeat"
|
||||
heartBeat.start()
|
||||
}
|
||||
|
||||
|
|
@ -106,3 +200,23 @@ struct LoadingView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func showAlert(title: String, message: String, showOk: Bool, completion: @escaping (Bool) -> Void) {
|
||||
DispatchQueue.main.async {
|
||||
if let mainWindow = UIApplication.shared.windows.last {
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
|
||||
if showOk {
|
||||
let okAction = UIAlertAction(title: "OK", style: .default) { _ in
|
||||
completion(true)
|
||||
}
|
||||
|
||||
alert.addAction(okAction)
|
||||
} else {
|
||||
completion(false)
|
||||
}
|
||||
|
||||
mainWindow.rootViewController?.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,13 @@
|
|||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
|
||||
extension UIDocumentPickerViewController {
|
||||
@objc func fix_init(forOpeningContentTypes contentTypes: [UTType], asCopy: Bool) -> UIDocumentPickerViewController {
|
||||
return fix_init(forOpeningContentTypes: contentTypes, asCopy: true)
|
||||
}
|
||||
}
|
||||
|
||||
struct HomeView: View {
|
||||
@AppStorage("username") private var username = "User"
|
||||
@AppStorage("customBackgroundColor") private var customBackgroundColorHex: String = Color.primaryBackground.toHex() ?? "#000000"
|
||||
|
|
@ -107,7 +114,7 @@ struct HomeView: View {
|
|||
}
|
||||
.fileImporter(
|
||||
isPresented: $isShowingPairingFilePicker,
|
||||
allowedContentTypes: [UTType(filenameExtension: "plist")!, UTType(filenameExtension: "mobiledevicepairing")!]
|
||||
allowedContentTypes: [UTType(filenameExtension: "plist")!, UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!]
|
||||
) { result in
|
||||
switch result {
|
||||
case .success(let url):
|
||||
|
|
@ -298,6 +305,24 @@ class InstalledAppsViewModel: ObservableObject {
|
|||
let output = String(cString: rawPointer)
|
||||
free(rawPointer)
|
||||
|
||||
// Try to parse JSON format first
|
||||
if let jsonData = output.data(using: .utf8) {
|
||||
do {
|
||||
let decoder = JSONDecoder()
|
||||
let appsDict = try decoder.decode([String: String].self, from: jsonData)
|
||||
if let app = appsDict.first, app.key == "error" {
|
||||
self.apps = []
|
||||
} else {
|
||||
self.apps = appsDict.map { AppInfo(bundleID: $0.key, name: $0.value) }
|
||||
}
|
||||
return
|
||||
} catch {
|
||||
print("Error decoding JSON, falling back to text parsing: \(error.localizedDescription)")
|
||||
// Fall through to text parsing below
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to text parsing
|
||||
if output.hasPrefix("Error:") {
|
||||
self.apps = []
|
||||
} else {
|
||||
|
|
@ -434,6 +459,7 @@ struct InstalledAppsListView: View {
|
|||
|
||||
// Helper method to load app icon
|
||||
private func loadAppIcon(for app: AppInfo) {
|
||||
// Use AppStoreIconFetcher for now
|
||||
AppStoreIconFetcher.getIcon(for: app.bundleID) { image in
|
||||
if let image = image {
|
||||
self.appIcons[app.bundleID] = image
|
||||
|
|
@ -446,7 +472,7 @@ struct InstalledAppsListView: View {
|
|||
}
|
||||
}
|
||||
|
||||
// App icon stuff - just fetches from App Store
|
||||
// Add this class before the #Preview section
|
||||
class AppStoreIconFetcher {
|
||||
static private var iconCache: [String: UIImage] = [:]
|
||||
|
||||
|
|
|
|||
70
StikJIT/Views/InstalledAppsListView.swift
Normal file
70
StikJIT/Views/InstalledAppsListView.swift
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
//
|
||||
// InstalledAppsListView.swift
|
||||
// StikJIT
|
||||
//
|
||||
// Created by Stossy11 on 28/03/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct InstalledAppsListView: View {
|
||||
@AppStorage("username") private var username = "User"
|
||||
@AppStorage("customBackgroundColor") private var customBackgroundColorHex: String = Color.primaryBackground.toHex() ?? "#000000"
|
||||
@State private var selectedBackgroundColor: Color = Color(hex: UserDefaults.standard.string(forKey: "customBackgroundColor") ?? "#000000") ?? Color.primaryBackground
|
||||
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
||||
@StateObject var viewModel = InstalledAppsViewModel()
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@State private var searchText: String = ""
|
||||
|
||||
var onSelect: (String) -> Void
|
||||
|
||||
var filteredApps: [String] {
|
||||
if searchText.isEmpty {
|
||||
return Array(viewModel.apps.keys) // Use the keys (app names)
|
||||
} else {
|
||||
return viewModel.apps.keys.filter { $0.localizedCaseInsensitiveContains(searchText) }
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ZStack {
|
||||
selectedBackgroundColor.edgesIgnoringSafeArea(.all)
|
||||
|
||||
List {
|
||||
ForEach(filteredApps, id: \.self) { app in
|
||||
Button(action: {
|
||||
onSelect(viewModel.apps[app] ?? "") // Select using the app's bundle ID
|
||||
}) {
|
||||
Text(app)
|
||||
.font(.system(.body, design: .rounded))
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
.listRowBackground(Color(.secondarySystemGroupedBackground))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
.listStyle(InsetGroupedListStyle())
|
||||
.navigationTitle("Installed Apps")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Done") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
.searchable(text: $searchText, prompt: "Search Apps")
|
||||
}
|
||||
.onReceive(timer) { _ in
|
||||
refreshBackground()
|
||||
}
|
||||
.onAppear {
|
||||
print(filteredApps)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshBackground() {
|
||||
selectedBackgroundColor = Color(hex: customBackgroundColorHex) ?? Color.primaryBackground
|
||||
}
|
||||
}
|
||||
|
|
@ -114,7 +114,7 @@ struct SettingsView: View {
|
|||
}
|
||||
.fileImporter(
|
||||
isPresented: $isShowingPairingFilePicker,
|
||||
allowedContentTypes: [UTType(filenameExtension: "plist")!, UTType(filenameExtension: "mobiledevicepairing")!]
|
||||
allowedContentTypes: [UTType(filenameExtension: "plist")!, UTType(filenameExtension: "mobiledevicepairing")!, .propertyList]
|
||||
) {result in
|
||||
switch result {
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,9 @@ char *list_installed_apps() {
|
|||
plist_t *app_list = (plist_t *)apps;
|
||||
char *result = malloc(16384); // Increased buffer size for additional data
|
||||
result[0] = '\0';
|
||||
strcat(result, "{\n");
|
||||
|
||||
int first_entry = 1;
|
||||
for (size_t i = 0; i < apps_len; i++) {
|
||||
plist_t app = app_list[i];
|
||||
// Check if the app has an "Entitlements" dictionary.
|
||||
|
|
@ -83,37 +85,59 @@ char *list_installed_apps() {
|
|||
if (bundle_id_node) {
|
||||
char *bundle_id = NULL;
|
||||
plist_get_string_val(bundle_id_node, &bundle_id);
|
||||
|
||||
// Get app name
|
||||
|
||||
// Skip if bundle ID is empty
|
||||
if (bundle_id == NULL || strlen(bundle_id) == 0) {
|
||||
free(bundle_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Retrieve the app name
|
||||
plist_t app_name_node = plist_dict_get_item(app, "CFBundleName");
|
||||
char *app_name = NULL;
|
||||
plist_t name_node = plist_dict_get_item(app, "CFBundleName");
|
||||
if (name_node) {
|
||||
plist_get_string_val(name_node, &app_name);
|
||||
if (app_name_node) {
|
||||
plist_get_string_val(app_name_node, &app_name);
|
||||
} else {
|
||||
name_node = plist_dict_get_item(app, "CFBundleDisplayName");
|
||||
if (name_node) {
|
||||
plist_get_string_val(name_node, &app_name);
|
||||
app_name = strdup("Unknown");
|
||||
}
|
||||
|
||||
// Escape special characters in app name and bundle ID
|
||||
char escaped_app_name[1024] = {0};
|
||||
char escaped_bundle_id[1024] = {0};
|
||||
for (int j = 0, k = 0; app_name[j] != '\0'; j++, k++) {
|
||||
if (app_name[j] == '"' || app_name[j] == '\\') {
|
||||
escaped_app_name[k++] = '\\';
|
||||
}
|
||||
escaped_app_name[k] = app_name[j];
|
||||
}
|
||||
|
||||
// Add bundle ID and app name to result
|
||||
strcat(result, bundle_id);
|
||||
strcat(result, "|"); // pipe as separator
|
||||
if (app_name) {
|
||||
strcat(result, app_name);
|
||||
free(app_name);
|
||||
} else {
|
||||
strcat(result, bundle_id); // Use bundle ID as fallback name
|
||||
for (int j = 0, k = 0; bundle_id[j] != '\0'; j++, k++) {
|
||||
if (bundle_id[j] == '"' || bundle_id[j] == '\\') {
|
||||
escaped_bundle_id[k++] = '\\';
|
||||
}
|
||||
escaped_bundle_id[k] = bundle_id[j];
|
||||
}
|
||||
strcat(result, "\n");
|
||||
|
||||
|
||||
// Add the app name and bundle ID to the result in JSON format
|
||||
if (!first_entry) {
|
||||
strcat(result, ",\n");
|
||||
}
|
||||
strcat(result, " \"");
|
||||
strcat(result, escaped_app_name);
|
||||
strcat(result, "\": \"");
|
||||
strcat(result, escaped_bundle_id);
|
||||
strcat(result, "\"");
|
||||
|
||||
first_entry = 0;
|
||||
free(bundle_id);
|
||||
free(app_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
strcat(result, "\n}\n");
|
||||
|
||||
installation_proxy_client_free(client);
|
||||
tcp_provider_free(provider);
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@
|
|||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <limits.h>
|
||||
|
||||
void startHeartbeat() {
|
||||
typedef void (^HeartbeatCompletionHandler)(int result, const char *message);
|
||||
|
||||
void startHeartbeat(HeartbeatCompletionHandler completion) {
|
||||
printf("DEBUG: Initializing logger...\n");
|
||||
idevice_init_logger(Debug, Disabled, NULL);
|
||||
|
||||
|
|
@ -43,6 +45,7 @@ void startHeartbeat() {
|
|||
IdeviceErrorCode err = idevice_pairing_file_read(pairingFilePath, &pairing_file);
|
||||
if (err != IdeviceSuccess) {
|
||||
fprintf(stderr, "DEBUG: Failed to read pairing file: %d\n", err);
|
||||
completion(err, "Failed to read pairing file");
|
||||
return;
|
||||
}
|
||||
printf("DEBUG: Pairing file read successfully.\n");
|
||||
|
|
@ -54,6 +57,7 @@ void startHeartbeat() {
|
|||
if (err != IdeviceSuccess) {
|
||||
fprintf(stderr, "DEBUG: Failed to create TCP provider: %d\n", err);
|
||||
idevice_pairing_file_free(pairing_file);
|
||||
completion(err, "Failed to create TCP provider");
|
||||
return;
|
||||
}
|
||||
printf("DEBUG: TCP provider created successfully.\n");
|
||||
|
|
@ -62,12 +66,15 @@ void startHeartbeat() {
|
|||
HeartbeatClientHandle *client = NULL;
|
||||
err = heartbeat_connect_tcp(provider, &client);
|
||||
if (err != IdeviceSuccess) {
|
||||
completion(err, "Failed to connect to Heartbeat");
|
||||
fprintf(stderr, "DEBUG: Failed to connect to installation proxy: %d\n", err);
|
||||
return;
|
||||
}
|
||||
tcp_provider_free(provider);
|
||||
printf("DEBUG: Connected to installation proxy successfully.\n");
|
||||
|
||||
completion(0, "Heartbeat Completed");
|
||||
|
||||
u_int64_t current_interval = 15;
|
||||
while (1) {
|
||||
u_int64_t new_interval = 0;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@
|
|||
#ifndef HEARTBEAT_H
|
||||
#define HEARTBEAT_H
|
||||
|
||||
void startHeartbeat();
|
||||
typedef void (^HeartbeatCompletionHandler)(int result, const char *message);
|
||||
|
||||
void startHeartbeat(HeartbeatCompletionHandler completion);
|
||||
|
||||
#endif /* HEARTBEAT_H */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue