mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-16 04:20:30 +00:00
Added Device Info
This commit is contained in:
parent
167e0362c3
commit
36c4b48233
6 changed files with 445 additions and 15 deletions
|
|
@ -6,3 +6,4 @@
|
|||
#include "idevice/idevice.h"
|
||||
#include "idevice/heartbeat.h"
|
||||
#include "JSSupport/JSSupport.h"
|
||||
#include "idevice/ideviceinfo.h"
|
||||
|
|
|
|||
275
StikJIT/Views/DeviceInfoManager.swift
Normal file
275
StikJIT/Views/DeviceInfoManager.swift
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
//
|
||||
// DeviceInfoManager.swift
|
||||
// StikDebug
|
||||
//
|
||||
// Created by Stephen on 8/2/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
@_silgen_name("ideviceinfo_c_init")
|
||||
private func c_deviceinfo_init(_ path: UnsafePointer<CChar>) -> Int32
|
||||
|
||||
@_silgen_name("ideviceinfo_c_get_xml")
|
||||
private func c_deviceinfo_get_xml() -> UnsafePointer<CChar>?
|
||||
|
||||
@_silgen_name("ideviceinfo_c_cleanup")
|
||||
private func c_deviceinfo_cleanup()
|
||||
|
||||
private func initErrorMessage(_ code: Int32) -> String {
|
||||
switch code {
|
||||
case 1: return "Couldn’t read pairingFile.plist"
|
||||
case 2: return "Unable to create device provider"
|
||||
case 3: return "Cannot connect to lockdown service"
|
||||
case 4: return "Unable to start lockdown session"
|
||||
default: return "Unknown init error (\(code))"
|
||||
}
|
||||
}
|
||||
|
||||
private let docs = FileManager.default
|
||||
.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
|
||||
@MainActor
|
||||
final class DeviceInfoManager: ObservableObject {
|
||||
@Published var entries: [(key: String, value: String)] = []
|
||||
@Published var busy = false
|
||||
@Published var error: (title: String, message: String)?
|
||||
|
||||
private var initialized = false
|
||||
|
||||
func initAndLoad() {
|
||||
guard !initialized else {
|
||||
loadInfo()
|
||||
return
|
||||
}
|
||||
busy = true
|
||||
let pairingPath = docs
|
||||
.appendingPathComponent("pairingFile.plist")
|
||||
.path
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let code = pairingPath.withCString { c_deviceinfo_init($0) }
|
||||
DispatchQueue.main.async {
|
||||
if code != 0 {
|
||||
self.error = ("Initialization Failed", initErrorMessage(code))
|
||||
self.busy = false
|
||||
} else {
|
||||
self.initialized = true
|
||||
self.loadInfo()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func loadInfo() {
|
||||
busy = true
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
guard let cXml = c_deviceinfo_get_xml() else {
|
||||
DispatchQueue.main.async {
|
||||
self.error = ("Fetch Error", "Failed to fetch device info")
|
||||
self.busy = false
|
||||
}
|
||||
return
|
||||
}
|
||||
defer { free(UnsafeMutableRawPointer(mutating: cXml)) }
|
||||
|
||||
guard let xml = String(validatingUTF8: cXml) else {
|
||||
DispatchQueue.main.async {
|
||||
self.error = ("Parse Error", "Invalid XML data")
|
||||
self.busy = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
guard let data = xml.data(using: .utf8) else {
|
||||
throw NSError(domain: "DeviceInfo", code: 0,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Invalid XML encoding"])
|
||||
}
|
||||
let plistObj = try PropertyListSerialization.propertyList(
|
||||
from: data,
|
||||
options: [], format: nil)
|
||||
guard let dict = plistObj as? [String: Any] else {
|
||||
throw NSError(domain: "DeviceInfo", code: 0,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Expected dictionary"])
|
||||
}
|
||||
|
||||
let formatted = dict.keys.sorted().map { key in
|
||||
(key, Self.convertToString(dict[key]!))
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.entries = formatted
|
||||
self.busy = false
|
||||
}
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
self.error = ("Parse Error", error.localizedDescription)
|
||||
self.busy = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cleanup() {
|
||||
c_deviceinfo_cleanup()
|
||||
initialized = false
|
||||
}
|
||||
|
||||
private static func convertToString(_ raw: Any) -> String {
|
||||
switch raw {
|
||||
case let s as String:
|
||||
return s
|
||||
case let n as NSNumber:
|
||||
return n.stringValue
|
||||
case let d as Data:
|
||||
let hex = d.prefix(32).map { String(format: "%02X", $0) }.joined()
|
||||
let suffix = d.count > 32 ? "…" : ""
|
||||
return "Data(\(d.count) B) \(hex)\(suffix)"
|
||||
case let arr as [Any]:
|
||||
return "[" + arr.map { convertToString($0) }.joined(separator: ", ") + "]"
|
||||
case let dic as [String: Any]:
|
||||
return "{" + dic.map { "\($0.key): \(convertToString($0.value))" }
|
||||
.joined(separator: ", ") + "}"
|
||||
default:
|
||||
return String(describing: raw)
|
||||
}
|
||||
}
|
||||
|
||||
func exportToCSV() throws -> URL {
|
||||
var csv = "Key,Value\n"
|
||||
for (key, value) in entries {
|
||||
let escKey = key.replacingOccurrences(of: "\"", with: "\"\"")
|
||||
let escValue = value.replacingOccurrences(of: "\"", with: "\"\"")
|
||||
csv += "\"\(escKey)\",\"\(escValue)\"\n"
|
||||
}
|
||||
let url = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("DeviceInfo-Export.csv")
|
||||
try csv.data(using: .utf8)?.write(to: url)
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
struct DeviceInfoView: View {
|
||||
@StateObject private var mgr = DeviceInfoManager()
|
||||
@State private var exportURL: URL?
|
||||
@State private var isShowingExporter = false
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ZStack {
|
||||
Color(UIColor.systemBackground).ignoresSafeArea()
|
||||
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
SettingsCard {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
|
||||
if mgr.entries.isEmpty {
|
||||
Text(mgr.busy ? "Loading…" : "No info available")
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
ForEach(mgr.entries, id: \.key) { entry in
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(entry.key)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Text(entry.value)
|
||||
.font(.footnote.monospaced())
|
||||
.foregroundColor(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.contextMenu {
|
||||
Button("Copy Key") {
|
||||
UIPasteboard.general.string = entry.key
|
||||
}
|
||||
Button("Copy Value") {
|
||||
UIPasteboard.general.string = entry.value
|
||||
}
|
||||
Button("Copy Both") {
|
||||
UIPasteboard.general.string = "\(entry.key): \(entry.value)"
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(16)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
.padding(.vertical, 20)
|
||||
}
|
||||
|
||||
if mgr.busy {
|
||||
Color.black.opacity(0.3).ignoresSafeArea()
|
||||
ProgressView()
|
||||
.padding()
|
||||
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 12))
|
||||
}
|
||||
|
||||
if let err = mgr.error {
|
||||
CustomErrorView(
|
||||
title: err.title,
|
||||
message: err.message,
|
||||
onDismiss: { mgr.error = nil }
|
||||
)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Device Info")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button(action: {
|
||||
do {
|
||||
let url = try mgr.exportToCSV()
|
||||
exportURL = url
|
||||
isShowingExporter = true
|
||||
} catch {
|
||||
mgr.error = ("Export Failed", error.localizedDescription)
|
||||
}
|
||||
}) {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
}
|
||||
.disabled(mgr.entries.isEmpty)
|
||||
}
|
||||
}
|
||||
.fileExporter(
|
||||
isPresented: $isShowingExporter,
|
||||
document: CSVDocument(url: exportURL),
|
||||
contentType: .commaSeparatedText,
|
||||
defaultFilename: "DeviceInfo"
|
||||
) { result in
|
||||
// completion
|
||||
}
|
||||
}
|
||||
.onAppear { mgr.initAndLoad() }
|
||||
.onDisappear { mgr.cleanup() }
|
||||
}
|
||||
}
|
||||
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
struct CSVDocument: FileDocument {
|
||||
static var readableContentTypes: [UTType] { [] }
|
||||
static var writableContentTypes: [UTType] { [UTType.commaSeparatedText] }
|
||||
|
||||
let url: URL?
|
||||
|
||||
init(url: URL?) {
|
||||
self.url = url
|
||||
}
|
||||
|
||||
init(configuration: ReadConfiguration) throws {
|
||||
fatalError("Not supported")
|
||||
}
|
||||
|
||||
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
|
||||
guard let url = url else {
|
||||
throw NSError(domain: "CSVDocument", code: -1, userInfo: nil)
|
||||
}
|
||||
return try FileWrapper(url: url, options: .immediate)
|
||||
}
|
||||
}
|
||||
|
|
@ -26,18 +26,18 @@ struct MainTabView: View {
|
|||
.tabItem {
|
||||
Label("Home", systemImage: "house")
|
||||
}
|
||||
if enableAdvancedOptions {
|
||||
ScriptListView()
|
||||
.tabItem {
|
||||
Label("Scripts", systemImage: "scroll")
|
||||
}
|
||||
}
|
||||
if enableTesting {
|
||||
IPAAppManagerView()
|
||||
.tabItem {
|
||||
Label("Testing", systemImage: "square.grid.2x2")
|
||||
}
|
||||
}
|
||||
DeviceInfoView()
|
||||
.tabItem {
|
||||
Label("Info", systemImage: "info.circle.fill")
|
||||
}
|
||||
SettingsView()
|
||||
.tabItem {
|
||||
Label("Settings", systemImage: "gearshape.fill")
|
||||
|
|
|
|||
|
|
@ -275,11 +275,6 @@ struct SettingsView: View {
|
|||
.foregroundColor(.primary)
|
||||
.padding(.bottom, 4)
|
||||
|
||||
Toggle("Enable Advanced Options", isOn: $enableAdvancedOptions)
|
||||
.foregroundColor(.primary)
|
||||
.padding(.vertical, 6)
|
||||
|
||||
if enableAdvancedOptions {
|
||||
Toggle("Run Default Script After Connecting", isOn: $useDefaultScript)
|
||||
.foregroundColor(.primary)
|
||||
.padding(.vertical, 6)
|
||||
|
|
@ -289,12 +284,6 @@ struct SettingsView: View {
|
|||
Toggle("Enable Advanced Beta Options", isOn: $enableAdvancedBetaOptions)
|
||||
.foregroundColor(.primary)
|
||||
.padding(.vertical, 6)
|
||||
if enableAdvancedBetaOptions {
|
||||
Toggle("Enable Self Developed Application Testing", isOn: $enableTesting)
|
||||
.foregroundColor(.primary)
|
||||
.padding(.vertical, 6)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 20)
|
||||
.padding(.horizontal, 16)
|
||||
|
|
|
|||
116
StikJIT/idevice/ideviceinfo.c
Normal file
116
StikJIT/idevice/ideviceinfo.c
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
//
|
||||
// ideviceinfo.c
|
||||
// StikDebug
|
||||
//
|
||||
// Created by Stephen on 8/2/25.
|
||||
//
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <arpa/inet.h>
|
||||
#include "ideviceinfo.h"
|
||||
#include "idevice.h"
|
||||
#include "plist.h"
|
||||
|
||||
static struct IdeviceProviderHandle * g_provider = NULL;
|
||||
static struct HeartbeatClientHandle * g_hb = NULL;
|
||||
static struct LockdowndClientHandle * g_client = NULL;
|
||||
static struct IdevicePairingFile * g_sess_pf = NULL;
|
||||
|
||||
int ideviceinfo_c_init(const char *pairing_file_path) {
|
||||
if (g_provider) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct IdevicePairingFile *pf = NULL;
|
||||
struct IdeviceFfiError *err = idevice_pairing_file_read(pairing_file_path, &pf);
|
||||
if (err) {
|
||||
idevice_error_free(err);
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct sockaddr_in sin = { .sin_family = AF_INET,
|
||||
.sin_port = htons(LOCKDOWN_PORT) };
|
||||
inet_pton(AF_INET, "10.7.0.2", &sin.sin_addr);
|
||||
|
||||
err = idevice_tcp_provider_new((const struct sockaddr *)&sin,
|
||||
pf,
|
||||
"ideviceinfo-c",
|
||||
&g_provider);
|
||||
if (err) {
|
||||
idevice_error_free(err);
|
||||
idevice_pairing_file_free(pf);
|
||||
return 2;
|
||||
}
|
||||
|
||||
err = heartbeat_connect(g_provider, &g_hb);
|
||||
if (!err) {
|
||||
heartbeat_send_polo(g_hb);
|
||||
} else {
|
||||
idevice_error_free(err);
|
||||
}
|
||||
|
||||
err = lockdownd_connect(g_provider, &g_client);
|
||||
if (err) {
|
||||
idevice_error_free(err);
|
||||
return 3;
|
||||
}
|
||||
|
||||
err = idevice_pairing_file_read(pairing_file_path, &g_sess_pf);
|
||||
if (err) {
|
||||
idevice_error_free(err);
|
||||
lockdownd_client_free(g_client);
|
||||
g_client = NULL;
|
||||
return 4;
|
||||
}
|
||||
|
||||
err = lockdownd_start_session(g_client, g_sess_pf);
|
||||
if (err) {
|
||||
idevice_error_free(err);
|
||||
lockdownd_client_free(g_client);
|
||||
g_client = NULL;
|
||||
return 4;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
char *ideviceinfo_c_get_xml(void) {
|
||||
if (!g_client) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void *plist_obj = NULL;
|
||||
struct IdeviceFfiError *err = lockdownd_get_all_values(g_client, NULL, &plist_obj);
|
||||
if (err) {
|
||||
idevice_error_free(err);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char *xml = NULL;
|
||||
uint32_t xml_len = 0;
|
||||
if (plist_to_xml(plist_obj, &xml, &xml_len) != 0 || !xml) {
|
||||
plist_free(plist_obj);
|
||||
return NULL;
|
||||
}
|
||||
plist_free(plist_obj);
|
||||
return xml;
|
||||
}
|
||||
|
||||
void ideviceinfo_c_cleanup(void) {
|
||||
if (g_client) {
|
||||
lockdownd_client_free(g_client);
|
||||
g_client = NULL;
|
||||
}
|
||||
if (g_sess_pf) {
|
||||
idevice_pairing_file_free(g_sess_pf);
|
||||
g_sess_pf = NULL;
|
||||
}
|
||||
if (g_hb) {
|
||||
heartbeat_client_free(g_hb);
|
||||
g_hb = NULL;
|
||||
}
|
||||
if (g_provider) {
|
||||
idevice_provider_free(g_provider);
|
||||
g_provider = NULL;
|
||||
}
|
||||
}
|
||||
49
StikJIT/idevice/ideviceinfo.h
Normal file
49
StikJIT/idevice/ideviceinfo.h
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
//
|
||||
// ideviceinfo.h
|
||||
// StikDebug
|
||||
//
|
||||
// Created by Stephen on 8/2/25.
|
||||
//
|
||||
|
||||
#ifndef IDEVICEINFO_H
|
||||
#define IDEVICEINFO_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Initialize the device connection:
|
||||
* - Reads the pairing file
|
||||
* - Creates a TCP provider
|
||||
* - Starts a persistent heartbeat
|
||||
* - Connects & starts a lockdown session
|
||||
*
|
||||
* @param pairing_file_path path to pairingFile.plist
|
||||
* @return 0 on success, non-zero on error
|
||||
*/
|
||||
int ideviceinfo_c_init(const char *pairing_file_path);
|
||||
|
||||
/**
|
||||
* Fetches all device info via the already-open lockdown session,
|
||||
* returning a malloc'd XML plist string (caller must free it).
|
||||
*
|
||||
* @return malloc'd XML on success, or NULL on error
|
||||
*/
|
||||
char *ideviceinfo_c_get_xml(void);
|
||||
|
||||
/**
|
||||
* Clean up everything:
|
||||
* - Stops heartbeat
|
||||
* - Frees lockdown client
|
||||
* - Frees provider & pairing file
|
||||
*/
|
||||
void ideviceinfo_c_cleanup(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // IDEVICEINFO_H
|
||||
Loading…
Add table
Add a link
Reference in a new issue