Added Device Info

This commit is contained in:
Stephen 2025-08-02 14:18:18 -04:00
parent 167e0362c3
commit 36c4b48233
6 changed files with 445 additions and 15 deletions

View file

@ -6,3 +6,4 @@
#include "idevice/idevice.h"
#include "idevice/heartbeat.h"
#include "JSSupport/JSSupport.h"
#include "idevice/ideviceinfo.h"

View 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)
}
}

View file

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

View file

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

View 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;
}
}

View 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