mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-16 04:20:30 +00:00
2.3.0 RC 3 (WIP)
This commit is contained in:
parent
e90ba11535
commit
f10f56e5e5
7 changed files with 572 additions and 103 deletions
|
|
@ -31,6 +31,9 @@ struct HomeView: View {
|
|||
@State private var isShowingInstalledApps = false
|
||||
@State private var isShowingPairingFilePicker = false
|
||||
@State private var pairingFileExists: Bool = false
|
||||
@State private var pairingFilePresentOnDisk: Bool = false
|
||||
@State private var isValidatingPairingFile = false
|
||||
@State private var lastValidatedPairingSignature: PairingFileSignature? = nil
|
||||
@State private var showPairingFileMessage = false
|
||||
@State private var pairingFileIsValid = false
|
||||
@State private var isImportingFile = false
|
||||
|
|
@ -58,7 +61,7 @@ struct HomeView: View {
|
|||
@State private var isLoadingQuickApps = false
|
||||
|
||||
@AppStorage("showiOS26Disclaimer") private var showiOS26Disclaimer: Bool = true
|
||||
|
||||
|
||||
@AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue
|
||||
@Environment(\.themeExpansionManager) private var themeExpansion
|
||||
private var backgroundStyle: BackgroundStyle { themeExpansion?.backgroundStyle(for: appThemeRaw) ?? AppTheme.system.backgroundStyle }
|
||||
|
|
@ -70,6 +73,8 @@ struct HomeView: View {
|
|||
|
||||
private var ddiMounted: Bool { isMounted() }
|
||||
private var canConnectByApp: Bool { pairingFileExists && ddiMounted }
|
||||
|
||||
private let pairingFileURL = URL.documentsDirectory.appendingPathComponent("pairingFile.plist")
|
||||
|
||||
private var isOnOrAfteriOS26: Bool {
|
||||
let v = ProcessInfo.processInfo.operatingSystemVersion
|
||||
|
|
@ -351,9 +356,13 @@ struct HomeView: View {
|
|||
|
||||
VStack(spacing: 10) {
|
||||
Button(action: primaryActionTapped) {
|
||||
whiteCardButtonLabel(icon: primaryActionIcon, title: primaryActionTitle, isLoading: isProcessing)
|
||||
whiteCardButtonLabel(
|
||||
icon: primaryActionIcon,
|
||||
title: primaryActionTitle,
|
||||
isLoading: isProcessing || isValidatingPairingFile
|
||||
)
|
||||
}
|
||||
.disabled(isProcessing)
|
||||
.disabled(isProcessing || isValidatingPairingFile)
|
||||
|
||||
if pairingFileExists && enableAdvancedOptions {
|
||||
Button(action: { showPIDSheet = true }) {
|
||||
|
|
@ -373,7 +382,23 @@ struct HomeView: View {
|
|||
}
|
||||
|
||||
private var readinessSummary: ReadinessSummary {
|
||||
if isValidatingPairingFile {
|
||||
return .init(
|
||||
title: "Validating pairing file",
|
||||
subtitle: "Hang tight while we verify the pairing file from your trusted computer.",
|
||||
icon: "hourglass.circle.fill",
|
||||
tint: .orange
|
||||
)
|
||||
}
|
||||
if !pairingFileExists {
|
||||
if pairingFilePresentOnDisk {
|
||||
return .init(
|
||||
title: "Pairing file needs attention",
|
||||
subtitle: "We found a pairing file but couldn’t read it. Re-import it from your trusted computer.",
|
||||
icon: "doc.badge.exclamationmark",
|
||||
tint: .yellow
|
||||
)
|
||||
}
|
||||
return .init(
|
||||
title: "Import your pairing file",
|
||||
subtitle: "Grab the pairing file from your computer to unlock app discovery.",
|
||||
|
|
@ -414,28 +439,59 @@ struct HomeView: View {
|
|||
}
|
||||
|
||||
private var primaryActionTitle: String {
|
||||
if !pairingFileExists { return "Import Pairing File" }
|
||||
if isValidatingPairingFile { return "Validating…" }
|
||||
if !pairingFileExists { return pairingFilePresentOnDisk ? "Re-import Pairing File" : "Import Pairing File" }
|
||||
if !ddiMounted { return "Mount Developer Disk Image" }
|
||||
return "Connect by App"
|
||||
}
|
||||
|
||||
private var primaryActionIcon: String {
|
||||
if !pairingFileExists { return "doc.badge.plus" }
|
||||
if isValidatingPairingFile { return "hourglass" }
|
||||
if !pairingFileExists { return pairingFilePresentOnDisk ? "arrow.clockwise" : "doc.badge.plus" }
|
||||
if !ddiMounted { return "externaldrive" }
|
||||
return "cable.connector.horizontal"
|
||||
}
|
||||
|
||||
private var readinessChecklist: [ChecklistItem] {
|
||||
let vpnConnected = tunnel.tunnelStatus == .connected
|
||||
let pairingItem: ChecklistItem
|
||||
|
||||
if isValidatingPairingFile {
|
||||
pairingItem = ChecklistItem(
|
||||
title: "Pairing file",
|
||||
subtitle: "Validating pairing file…",
|
||||
status: .waiting,
|
||||
actionTitle: nil,
|
||||
action: nil
|
||||
)
|
||||
} else if pairingFileExists {
|
||||
pairingItem = ChecklistItem(
|
||||
title: "Pairing file",
|
||||
subtitle: "Imported and valid.",
|
||||
status: .ready,
|
||||
actionTitle: nil,
|
||||
action: nil
|
||||
)
|
||||
} else if pairingFilePresentOnDisk {
|
||||
pairingItem = ChecklistItem(
|
||||
title: "Pairing file",
|
||||
subtitle: "We couldn’t read the pairing file that’s on disk. Re-import it from your trusted computer.",
|
||||
status: .attention,
|
||||
actionTitle: "Re-import",
|
||||
action: { isShowingPairingFilePicker = true }
|
||||
)
|
||||
} else {
|
||||
pairingItem = ChecklistItem(
|
||||
title: "Pairing file",
|
||||
subtitle: "Import the pairing file generated from your trusted computer.",
|
||||
status: .actionRequired,
|
||||
actionTitle: "Import",
|
||||
action: { isShowingPairingFilePicker = true }
|
||||
)
|
||||
}
|
||||
|
||||
return [
|
||||
ChecklistItem(
|
||||
title: "Pairing file",
|
||||
subtitle: pairingFileExists ? "Imported and valid." : "Import the pairing file generated from your trusted computer.",
|
||||
status: pairingFileExists ? .ready : .actionRequired,
|
||||
actionTitle: pairingFileExists ? nil : "Import",
|
||||
action: pairingFileExists ? nil : { isShowingPairingFilePicker = true }
|
||||
),
|
||||
pairingItem,
|
||||
ChecklistItem(
|
||||
title: "Developer Disk Image",
|
||||
subtitle: ddiMounted ? "Mounted successfully." : "Open Settings → Developer Disk Image and tap Mount.",
|
||||
|
|
@ -755,6 +811,7 @@ struct HomeView: View {
|
|||
}
|
||||
|
||||
private func primaryActionTapped() {
|
||||
guard !isValidatingPairingFile else { return }
|
||||
if pairingFileExists {
|
||||
if !ddiMounted {
|
||||
showAlert(title: "Device Not Mounted".localized, message: "The Developer Disk Image has not been mounted yet. Check in settings for more information.".localized, showOk: true) { _ in }
|
||||
|
|
@ -790,8 +847,43 @@ struct HomeView: View {
|
|||
}
|
||||
|
||||
private func checkPairingFileExists() {
|
||||
let fileExists = FileManager.default.fileExists(atPath: URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path)
|
||||
pairingFileExists = fileExists && isPairing()
|
||||
let fileExists = FileManager.default.fileExists(atPath: pairingFileURL.path)
|
||||
pairingFilePresentOnDisk = fileExists
|
||||
|
||||
guard fileExists else {
|
||||
pairingFileExists = false
|
||||
lastValidatedPairingSignature = nil
|
||||
isValidatingPairingFile = false
|
||||
return
|
||||
}
|
||||
|
||||
let signature = pairingFileSignature(for: pairingFileURL)
|
||||
|
||||
guard needsValidation(for: signature) else { return }
|
||||
guard !isValidatingPairingFile else { return }
|
||||
|
||||
isValidatingPairingFile = true
|
||||
|
||||
DispatchQueue.global(qos: .utility).async {
|
||||
let valid = isPairing()
|
||||
DispatchQueue.main.async {
|
||||
pairingFileExists = valid
|
||||
lastValidatedPairingSignature = signature
|
||||
isValidatingPairingFile = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func needsValidation(for signature: PairingFileSignature) -> Bool {
|
||||
guard let lastSignature = lastValidatedPairingSignature else { return true }
|
||||
return lastSignature != signature
|
||||
}
|
||||
|
||||
private func pairingFileSignature(for url: URL) -> PairingFileSignature {
|
||||
let attributes = (try? FileManager.default.attributesOfItem(atPath: url.path)) ?? [:]
|
||||
let modificationDate = attributes[.modificationDate] as? Date
|
||||
let sizeValue = (attributes[.size] as? NSNumber)?.uint64Value ?? 0
|
||||
return PairingFileSignature(modificationDate: modificationDate, fileSize: sizeValue)
|
||||
}
|
||||
private func refreshBackground() { }
|
||||
|
||||
|
|
@ -1095,6 +1187,11 @@ private struct ReadinessSummary {
|
|||
let tint: Color
|
||||
}
|
||||
|
||||
private struct PairingFileSignature: Equatable {
|
||||
let modificationDate: Date?
|
||||
let fileSize: UInt64
|
||||
}
|
||||
|
||||
private enum ChecklistStatus {
|
||||
case ready
|
||||
case waiting
|
||||
|
|
|
|||
|
|
@ -28,12 +28,16 @@ struct InstalledAppsListView: View {
|
|||
}
|
||||
}
|
||||
|
||||
@AppStorage("loadAppIconsOnJIT") private var loadAppIconsOnJIT = true
|
||||
@AppStorage("performanceMode") private var performanceMode = false
|
||||
@State private var showPerformanceToast = false
|
||||
@State private var launchingBundles: Set<String> = []
|
||||
@State private var launchFeedback: LaunchFeedback? = nil
|
||||
@State private var debuggableSearchText: String = ""
|
||||
@State private var otherSearchText: String = ""
|
||||
@State private var prefetchedBundleIDs: Set<String> = []
|
||||
@State private var selectedTab: AppListTab = .debuggable
|
||||
@State private var systemSearchText: String = ""
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
var onSelectApp: (String) -> Void
|
||||
|
|
@ -78,6 +82,7 @@ struct InstalledAppsListView: View {
|
|||
|
||||
private enum AppListTab: String, CaseIterable, Identifiable {
|
||||
case debuggable
|
||||
case system
|
||||
case other
|
||||
|
||||
var id: String { rawValue }
|
||||
|
|
@ -85,6 +90,7 @@ struct InstalledAppsListView: View {
|
|||
var title: String {
|
||||
switch self {
|
||||
case .debuggable: return "Debuggable"
|
||||
case .system: return "System Apps"
|
||||
case .other: return "Other"
|
||||
}
|
||||
}
|
||||
|
|
@ -106,6 +112,16 @@ struct InstalledAppsListView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private var sortedSystemApps: [(key: String, value: String)] {
|
||||
viewModel.systemApps.sorted { lhs, rhs in
|
||||
let comparison = lhs.value.localizedCaseInsensitiveCompare(rhs.value)
|
||||
if comparison == .orderedSame {
|
||||
return lhs.key < rhs.key
|
||||
}
|
||||
return comparison == .orderedAscending
|
||||
}
|
||||
}
|
||||
|
||||
private var otherSearchIsActive: Bool {
|
||||
!otherSearchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
}
|
||||
|
|
@ -118,6 +134,18 @@ struct InstalledAppsListView: View {
|
|||
return base.filter { matches(query, bundleID: $0.key, name: $0.value) }
|
||||
}
|
||||
|
||||
private var systemSearchIsActive: Bool {
|
||||
!systemSearchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
}
|
||||
|
||||
private var filteredSystemApps: [(key: String, value: String)] {
|
||||
let base = sortedSystemApps
|
||||
guard systemSearchIsActive else { return base }
|
||||
let query = normalizedSearchString(systemSearchText)
|
||||
guard !query.isEmpty else { return base }
|
||||
return base.filter { matches(query, bundleID: $0.key, name: $0.value) }
|
||||
}
|
||||
|
||||
private func normalizedSearchString(_ text: String) -> String {
|
||||
text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.folding(options: [.caseInsensitive, .diacriticInsensitive], locale: .current)
|
||||
|
|
@ -137,6 +165,8 @@ struct InstalledAppsListView: View {
|
|||
return viewModel.debuggableApps.isEmpty
|
||||
case .other:
|
||||
return filteredOtherApps.isEmpty
|
||||
case .system:
|
||||
return filteredSystemApps.isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -232,6 +262,20 @@ struct InstalledAppsListView: View {
|
|||
}
|
||||
}
|
||||
.preferredColorScheme(preferredScheme)
|
||||
.onAppear {
|
||||
prefetchedBundleIDs.removeAll()
|
||||
prefetchPriorityIcons()
|
||||
}
|
||||
.onChange(of: favoriteApps) { _, _ in prefetchPriorityIcons() }
|
||||
.onChange(of: recentApps) { _, _ in prefetchPriorityIcons() }
|
||||
.onChange(of: viewModel.isLoading) { _, newValue in
|
||||
if newValue {
|
||||
prefetchedBundleIDs.removeAll()
|
||||
} else {
|
||||
prefetchPriorityIcons()
|
||||
}
|
||||
}
|
||||
.onChange(of: selectedTab) { _, _ in prefetchPriorityIcons() }
|
||||
}
|
||||
|
||||
// MARK: Empty State
|
||||
|
|
@ -258,6 +302,18 @@ struct InstalledAppsListView: View {
|
|||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal)
|
||||
case .system:
|
||||
Text("No Hidden System Apps".localized)
|
||||
.font(.title2.weight(.semibold))
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
Text("""
|
||||
Hidden system apps appear here once CoreDevice exposes them. Try launching a bundle from the Other tab to surface background services.
|
||||
""".localized)
|
||||
.font(.body)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal)
|
||||
case .other:
|
||||
Text("Nothing To Show".localized)
|
||||
.font(.title2.weight(.semibold))
|
||||
|
|
@ -275,7 +331,18 @@ struct InstalledAppsListView: View {
|
|||
.padding(24)
|
||||
.glassCard(cornerRadius: 24, material: .thinMaterial, strokeOpacity: 0.12)
|
||||
.accessibilityElement(children: .combine)
|
||||
.accessibilityLabel("Empty list. Import pairing file or sign app appropriately.".localized)
|
||||
.accessibilityLabel(emptyStateAccessibilityLabel(for: tab).localized)
|
||||
}
|
||||
|
||||
private func emptyStateAccessibilityLabel(for tab: AppListTab) -> String {
|
||||
switch tab {
|
||||
case .debuggable:
|
||||
return "No debuggable apps available"
|
||||
case .system:
|
||||
return "No hidden system apps available"
|
||||
case .other:
|
||||
return "No other apps available"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Apps List
|
||||
|
|
@ -345,36 +412,84 @@ struct InstalledAppsListView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func otherSections(apps: [(key: String, value: String)]) -> some View {
|
||||
VStack(spacing: 18) {
|
||||
glassSection(title: "How It Works".localized) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Launch apps without get-task-allow by tunneling through CoreDevice. This only starts the app; attach debugging from Recents once it exposes get-task-allow.".localized)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
private func prefetchPriorityIcons(limit: Int = 32) {
|
||||
guard loadAppIconsOnJIT else { return }
|
||||
|
||||
glassSection(title: "All Installed Apps".localized) {
|
||||
LazyVStack(spacing: 12) {
|
||||
ForEach(apps, id: \.key) { bundleID, appName in
|
||||
LaunchAppRow(
|
||||
bundleID: bundleID,
|
||||
appName: appName,
|
||||
isLaunching: launchingBundles.contains(bundleID),
|
||||
appIcons: $appIcons,
|
||||
performanceMode: performanceMode
|
||||
) {
|
||||
startLaunching(bundleID: bundleID)
|
||||
}
|
||||
var priorityIDs: [String] = []
|
||||
var seen = Set<String>()
|
||||
|
||||
func appendUnique<S: Sequence>(_ ids: S) where S.Element == String {
|
||||
guard priorityIDs.count < limit else { return }
|
||||
for id in ids {
|
||||
guard seen.insert(id).inserted else { continue }
|
||||
priorityIDs.append(id)
|
||||
if priorityIDs.count >= limit { break }
|
||||
}
|
||||
}
|
||||
|
||||
appendUnique(favoriteApps)
|
||||
appendUnique(recentApps)
|
||||
appendUnique(debuggableSortedApps.map { $0.key })
|
||||
appendUnique(sortedSystemApps.map { $0.key })
|
||||
appendUnique(sortedOtherApps.map { $0.key })
|
||||
|
||||
let toPrefetch = priorityIDs.filter { !prefetchedBundleIDs.contains($0) }
|
||||
guard !toPrefetch.isEmpty else { return }
|
||||
|
||||
prefetchedBundleIDs.formUnion(toPrefetch)
|
||||
IconCache.shared.prefetchIcons(for: toPrefetch)
|
||||
}
|
||||
|
||||
private func otherSections(apps: [(key: String, value: String)]) -> some View {
|
||||
glassSection(title: "Installed Apps".localized) {
|
||||
LazyVStack(spacing: 12) {
|
||||
ForEach(apps, id: \.key) { bundleID, appName in
|
||||
LaunchAppRow(
|
||||
bundleID: bundleID,
|
||||
appName: appName,
|
||||
isLaunching: launchingBundles.contains(bundleID),
|
||||
appIcons: $appIcons,
|
||||
performanceMode: performanceMode
|
||||
) {
|
||||
startLaunching(bundleID: bundleID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func systemSections(apps: [(key: String, value: String)]) -> some View {
|
||||
glassSection(title: "Hidden System Apps".localized) {
|
||||
LazyVStack(spacing: 12) {
|
||||
ForEach(apps, id: \.key) { bundleID, appName in
|
||||
LaunchAppRow(
|
||||
bundleID: bundleID,
|
||||
appName: appName,
|
||||
isLaunching: launchingBundles.contains(bundleID),
|
||||
appIcons: $appIcons,
|
||||
performanceMode: performanceMode
|
||||
) {
|
||||
startLaunching(bundleID: bundleID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var tabbedContent: some View {
|
||||
tabContent(for: .debuggable)
|
||||
VStack(spacing: 14) {
|
||||
Picker("", selection: $selectedTab) {
|
||||
ForEach(AppListTab.allCases) { tab in
|
||||
Text(tab.title.localized)
|
||||
.tag(tab)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
tabContent(for: selectedTab)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
|
@ -415,6 +530,8 @@ struct InstalledAppsListView: View {
|
|||
.padding(.vertical, 24)
|
||||
}
|
||||
}
|
||||
case .system:
|
||||
systemTabContent(apps: filteredSystemApps)
|
||||
case .other:
|
||||
otherTabContent(apps: filteredOtherApps)
|
||||
}
|
||||
|
|
@ -446,6 +563,32 @@ struct InstalledAppsListView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func systemTabContent(apps: [(key: String, value: String)]) -> some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 18) {
|
||||
systemSearchBar
|
||||
|
||||
if let error = viewModel.lastError {
|
||||
errorBanner(error)
|
||||
}
|
||||
|
||||
if apps.isEmpty {
|
||||
if systemSearchIsActive {
|
||||
systemSearchEmptyState
|
||||
.transition(.opacity.combined(with: .scale))
|
||||
} else {
|
||||
emptyState(for: .system)
|
||||
.transition(.opacity.combined(with: .scale))
|
||||
}
|
||||
} else {
|
||||
systemSections(apps: apps)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 24)
|
||||
}
|
||||
}
|
||||
|
||||
private var debuggableSearchBar: some View {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
|
|
@ -544,6 +687,55 @@ struct InstalledAppsListView: View {
|
|||
)
|
||||
}
|
||||
|
||||
private var systemSearchBar: some View {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
TextField("Search system apps".localized, text: $systemSearchText)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled(true)
|
||||
|
||||
if !systemSearchText.isEmpty {
|
||||
Button {
|
||||
systemSearchText = ""
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.system(size: 16))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.accessibilityLabel("Clear search".localized)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 10)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||
.fill(.ultraThinMaterial)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||
.stroke(.white.opacity(0.08), lineWidth: 1)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private var systemSearchEmptyState: some View {
|
||||
VStack(spacing: 12) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.font(.system(size: 44, weight: .regular))
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
Text("No hidden matches".localized)
|
||||
.font(.title3.weight(.semibold))
|
||||
|
||||
Text("Try another bundle identifier or internal process name.".localized)
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(24)
|
||||
.glassCard(cornerRadius: 20, material: .thinMaterial, strokeOpacity: 0.12)
|
||||
}
|
||||
|
||||
private func errorBanner(_ message: String) -> some View {
|
||||
Text(message)
|
||||
.font(.footnote)
|
||||
|
|
@ -818,7 +1010,8 @@ struct AppButton: View {
|
|||
|
||||
private func loadAppIcon(for bundleID: String) {
|
||||
guard loadAppIconsOnJIT else { return }
|
||||
IconCache.shared.fetchIcon(for: bundleID) { image in
|
||||
guard appIcons[bundleID] == nil else { return }
|
||||
IconCache.shared.fetchIcon(for: bundleID, priority: .veryHigh) { image in
|
||||
guard let image else { return }
|
||||
let shouldAnimate = appIcons[bundleID] == nil
|
||||
if shouldAnimate {
|
||||
|
|
@ -934,7 +1127,9 @@ struct LaunchAppRow: View {
|
|||
}
|
||||
|
||||
private func loadAppIcon(for bundleID: String) {
|
||||
IconCache.shared.fetchIcon(for: bundleID) { image in
|
||||
guard loadAppIconsOnJIT else { return }
|
||||
guard appIcons[bundleID] == nil else { return }
|
||||
IconCache.shared.fetchIcon(for: bundleID, priority: .veryHigh) { image in
|
||||
guard let image else { return }
|
||||
let shouldAnimate = appIcons[bundleID] == nil
|
||||
if shouldAnimate {
|
||||
|
|
@ -1024,6 +1219,7 @@ final class IconCache {
|
|||
private let queue = OperationQueue()
|
||||
private let callbackQueue = DispatchQueue(label: "com.stik.iconcache.callbacks")
|
||||
private var pendingCallbacks: [String: [(UIImage?) -> Void]] = [:]
|
||||
private var pendingOperations: [String: Operation] = [:]
|
||||
|
||||
private init() {
|
||||
queue.maxConcurrentOperationCount = 4
|
||||
|
|
@ -1032,7 +1228,11 @@ final class IconCache {
|
|||
mem.totalCostLimit = 64 * 1024 * 1024
|
||||
}
|
||||
|
||||
func fetchIcon(for bundleID: String, completion: @escaping (UIImage?) -> Void) {
|
||||
func fetchIcon(
|
||||
for bundleID: String,
|
||||
priority: Operation.QueuePriority = .normal,
|
||||
completion: @escaping (UIImage?) -> Void
|
||||
) {
|
||||
if let memImage = mem.object(forKey: bundleID as NSString) {
|
||||
DispatchQueue.main.async { completion(memImage) }
|
||||
return
|
||||
|
|
@ -1043,6 +1243,9 @@ final class IconCache {
|
|||
if var existing = pendingCallbacks[bundleID] {
|
||||
existing.append(completion)
|
||||
pendingCallbacks[bundleID] = existing
|
||||
if let op = pendingOperations[bundleID], op.queuePriority.rawValue < priority.rawValue {
|
||||
op.queuePriority = priority
|
||||
}
|
||||
} else {
|
||||
pendingCallbacks[bundleID] = [completion]
|
||||
shouldStartLoad = true
|
||||
|
|
@ -1051,7 +1254,7 @@ final class IconCache {
|
|||
|
||||
guard shouldStartLoad else { return }
|
||||
|
||||
queue.addOperation { [weak self] in
|
||||
let operation = BlockOperation { [weak self] in
|
||||
guard let self else { return }
|
||||
|
||||
var loadedFromDisk = false
|
||||
|
|
@ -1070,16 +1273,19 @@ final class IconCache {
|
|||
}
|
||||
|
||||
if let image = result {
|
||||
let cost = Int(image.size.width * image.size.height * image.scale * image.scale)
|
||||
self.mem.setObject(image, forKey: bundleID as NSString, cost: cost)
|
||||
let prepared = self.prepareForDisplay(image)
|
||||
let cost = Int(prepared.size.width * prepared.size.height * prepared.scale * prepared.scale)
|
||||
self.mem.setObject(prepared, forKey: bundleID as NSString, cost: cost)
|
||||
if !loadedFromDisk {
|
||||
saveIconToGroup(image, bundleID: bundleID)
|
||||
saveIconToGroup(prepared, bundleID: bundleID)
|
||||
}
|
||||
result = prepared
|
||||
}
|
||||
|
||||
let callbacks = self.callbackQueue.sync { () -> [(UIImage?) -> Void] in
|
||||
let handlers = self.pendingCallbacks[bundleID] ?? []
|
||||
self.pendingCallbacks[bundleID] = nil
|
||||
self.pendingOperations[bundleID] = nil
|
||||
return handlers
|
||||
}
|
||||
|
||||
|
|
@ -1087,6 +1293,20 @@ final class IconCache {
|
|||
callbacks.forEach { $0(result) }
|
||||
}
|
||||
}
|
||||
operation.queuePriority = priority
|
||||
|
||||
callbackQueue.sync {
|
||||
pendingOperations[bundleID] = operation
|
||||
}
|
||||
|
||||
queue.addOperation(operation)
|
||||
}
|
||||
|
||||
func prefetchIcons(for bundleIDs: [String]) {
|
||||
let uniqueIDs = Set(bundleIDs)
|
||||
for bundleID in uniqueIDs {
|
||||
fetchIcon(for: bundleID, priority: .veryLow) { _ in }
|
||||
}
|
||||
}
|
||||
|
||||
private func diskCachedIcon(for bundleID: String) -> UIImage? {
|
||||
|
|
@ -1097,7 +1317,25 @@ final class IconCache {
|
|||
let iconsDir = containerURL.appendingPathComponent("icons", isDirectory: true)
|
||||
let fileURL = iconsDir.appendingPathComponent("\(bundleID).png")
|
||||
guard FileManager.default.fileExists(atPath: fileURL.path) else { return nil }
|
||||
return UIImage(contentsOfFile: fileURL.path)
|
||||
|
||||
guard let data = try? Data(contentsOf: fileURL) else {
|
||||
try? FileManager.default.removeItem(at: fileURL)
|
||||
return nil
|
||||
}
|
||||
|
||||
if let image = UIImage(data: data, scale: UIScreen.main.scale) {
|
||||
return image
|
||||
}
|
||||
|
||||
try? FileManager.default.removeItem(at: fileURL)
|
||||
return nil
|
||||
}
|
||||
|
||||
private func prepareForDisplay(_ image: UIImage) -> UIImage {
|
||||
if #available(iOS 15.0, *) {
|
||||
return image.preparingForDisplay() ?? image
|
||||
}
|
||||
return image
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1184,6 +1422,7 @@ extension Array: @retroactive RawRepresentable where Element: Codable {
|
|||
class InstalledAppsViewModel: ObservableObject {
|
||||
@Published var debuggableApps: [String: String] = [:]
|
||||
@Published var nonDebuggableApps: [String: String] = [:]
|
||||
@Published var systemApps: [String: String] = [:]
|
||||
@Published var isLoading = false
|
||||
@Published var lastError: String? = nil
|
||||
|
||||
|
|
@ -1200,19 +1439,35 @@ class InstalledAppsViewModel: ObservableObject {
|
|||
do {
|
||||
let debuggable = try JITEnableContext.shared.getAppList()
|
||||
let allApps = try JITEnableContext.shared.getAllApps()
|
||||
let hiddenSystem = (try? JITEnableContext.shared.getHiddenSystemApps()) ?? [:]
|
||||
|
||||
let nonDebuggableSequence = allApps.filter { debuggable[$0.key] == nil }
|
||||
let nonDebuggable = Dictionary(uniqueKeysWithValues: nonDebuggableSequence.map { ($0.key, $0.value) })
|
||||
var nonDebuggable: [String: String] = [:]
|
||||
var system: [String: String] = [:]
|
||||
|
||||
for (bundle, name) in nonDebuggableSequence {
|
||||
if let hiddenName = hiddenSystem[bundle] {
|
||||
system[bundle] = hiddenName
|
||||
} else {
|
||||
nonDebuggable[bundle] = name
|
||||
}
|
||||
}
|
||||
|
||||
for (bundle, name) in hiddenSystem where system[bundle] == nil && debuggable[bundle] == nil {
|
||||
system[bundle] = name
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.debuggableApps = debuggable
|
||||
self.nonDebuggableApps = nonDebuggable
|
||||
self.systemApps = system
|
||||
self.isLoading = false
|
||||
}
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
self.debuggableApps = [:]
|
||||
self.nonDebuggableApps = [:]
|
||||
self.systemApps = [:]
|
||||
self.isLoading = false
|
||||
self.lastError = error.localizedDescription
|
||||
print("Failed to load apps: \(error)")
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ typedef void (^SyslogErrorHandler)(NSError *error);
|
|||
- (BOOL)debugAppWithPID:(int)pid logger:(LogFunc)logger jsCallback:(DebugAppCallback)jsCallback;
|
||||
- (NSDictionary<NSString*, NSString*>*)getAppListWithError:(NSError**)error;
|
||||
- (NSDictionary<NSString*, NSString*>*)getAllAppsWithError:(NSError**)error;
|
||||
- (NSDictionary<NSString*, NSString*>*)getHiddenSystemAppsWithError:(NSError**)error;
|
||||
- (UIImage*)getAppIconWithBundleId:(NSString*)bundleId error:(NSError**)error;
|
||||
- (BOOL)launchAppWithoutDebug:(NSString*)bundleID logger:(LogFunc)logger;
|
||||
- (void)startSyslogRelayWithHandler:(SyslogLineHandler)lineHandler
|
||||
|
|
|
|||
|
|
@ -203,6 +203,22 @@ JITEnableContext* sharedJITContext = nil;
|
|||
return apps;
|
||||
}
|
||||
|
||||
- (NSDictionary<NSString*, NSString*>*)getHiddenSystemAppsWithError:(NSError**)error {
|
||||
if (!provider) {
|
||||
NSLog(@"Provider not initialized!");
|
||||
*error = [self errorWithStr:@"Provider not initialized!" code:-1];
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString* errorStr = nil;
|
||||
NSDictionary<NSString*, NSString*>* apps = list_hidden_system_apps(provider, &errorStr);
|
||||
if (errorStr) {
|
||||
*error = [self errorWithStr:errorStr code:-17];
|
||||
return nil;
|
||||
}
|
||||
return apps;
|
||||
}
|
||||
|
||||
- (UIImage*)getAppIconWithBundleId:(NSString*)bundleId error:(NSError**)error {
|
||||
if (!provider) {
|
||||
NSLog(@"Provider not initialized!");
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
|
||||
NSDictionary<NSString*, NSString*>* list_installed_apps(IdeviceProviderHandle* provider, NSString** error);
|
||||
NSDictionary<NSString*, NSString*>* list_all_apps(IdeviceProviderHandle* provider, NSString** error);
|
||||
NSDictionary<NSString*, NSString*>* list_hidden_system_apps(IdeviceProviderHandle* provider, NSString** error);
|
||||
UIImage* getAppIcon(IdeviceProviderHandle* provider, NSString* bundleID, NSString** error);
|
||||
|
||||
#endif /* APPLIST_H */
|
||||
|
|
|
|||
|
|
@ -11,9 +11,101 @@
|
|||
#include <string.h>
|
||||
#import "applist.h"
|
||||
|
||||
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];
|
||||
free(displayNameC);
|
||||
return displayName;
|
||||
}
|
||||
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];
|
||||
free(nameC);
|
||||
return name;
|
||||
}
|
||||
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 requireGetTaskAllow,
|
||||
BOOL (^filter)(plist_t app))
|
||||
{
|
||||
NSMutableDictionary<NSString*, NSString*> *result = [NSMutableDictionary dictionaryWithCapacity:count];
|
||||
|
||||
|
|
@ -31,6 +123,10 @@ static NSDictionary<NSString*, NSString*> *buildAppDictionary(void *apps,
|
|||
if (!isAllowed) continue;
|
||||
}
|
||||
|
||||
if (filter && !filter(app)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
plist_t bidNode = plist_dict_get_item(app, "CFBundleIdentifier");
|
||||
if (!bidNode) continue;
|
||||
|
||||
|
|
@ -44,18 +140,7 @@ static NSDictionary<NSString*, NSString*> *buildAppDictionary(void *apps,
|
|||
NSString *bundleID = [NSString stringWithUTF8String:bidC];
|
||||
free(bidC);
|
||||
|
||||
NSString *appName = @"Unknown";
|
||||
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') {
|
||||
appName = [NSString stringWithUTF8String:nameC];
|
||||
}
|
||||
free(nameC);
|
||||
}
|
||||
|
||||
result[bundleID] = appName;
|
||||
result[bundleID] = extractAppName(app);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
@ -63,7 +148,8 @@ static NSDictionary<NSString*, NSString*> *buildAppDictionary(void *apps,
|
|||
|
||||
static NSDictionary<NSString*, NSString*> *performAppQuery(IdeviceProviderHandle *provider,
|
||||
BOOL requireGetTaskAllow,
|
||||
NSString **error)
|
||||
NSString **error,
|
||||
BOOL (^filter)(plist_t app))
|
||||
{
|
||||
InstallationProxyClientHandle *client = NULL;
|
||||
if (installation_proxy_connect_tcp(provider, &client)) {
|
||||
|
|
@ -79,17 +165,23 @@ static NSDictionary<NSString*, NSString*> *performAppQuery(IdeviceProviderHandle
|
|||
return nil;
|
||||
}
|
||||
|
||||
NSDictionary<NSString*, NSString*> *result = buildAppDictionary(apps, count, requireGetTaskAllow);
|
||||
NSDictionary<NSString*, NSString*> *result = buildAppDictionary(apps, count, requireGetTaskAllow, filter);
|
||||
installation_proxy_client_free(client);
|
||||
return result;
|
||||
}
|
||||
|
||||
NSDictionary<NSString*, NSString*>* list_installed_apps(IdeviceProviderHandle* provider, NSString** error) {
|
||||
return performAppQuery(provider, YES, error);
|
||||
return performAppQuery(provider, YES, error, nil);
|
||||
}
|
||||
|
||||
NSDictionary<NSString*, NSString*>* list_all_apps(IdeviceProviderHandle* provider, NSString** error) {
|
||||
return performAppQuery(provider, NO, error);
|
||||
return performAppQuery(provider, NO, error, nil);
|
||||
}
|
||||
|
||||
NSDictionary<NSString*, NSString*>* list_hidden_system_apps(IdeviceProviderHandle* provider, NSString** error) {
|
||||
return performAppQuery(provider, NO, error, ^BOOL(plist_t app) {
|
||||
return isHiddenSystemApp(app);
|
||||
});
|
||||
}
|
||||
|
||||
UIImage* getAppIcon(IdeviceProviderHandle* provider, NSString* bundleID, NSString** error) {
|
||||
|
|
|
|||
|
|
@ -112,8 +112,10 @@ int debug_app(IdeviceProviderHandle* tcp_provider, const char *bundle_id, LogFun
|
|||
fprintf(stderr, "Failed to create TCP adapter: [%d] %s\n", err->code,
|
||||
err->message);
|
||||
idevice_error_free(err);
|
||||
core_device_proxy_free(core_device);
|
||||
return 1;
|
||||
}
|
||||
core_device = NULL; // adapter takes ownership of the proxy
|
||||
|
||||
AdapterStreamHandle *stream = NULL;
|
||||
err = adapter_connect(adapter, rsd_port, (ReadWriteOpaque **)&stream);
|
||||
|
|
@ -138,6 +140,7 @@ int debug_app(IdeviceProviderHandle* tcp_provider, const char *bundle_id, LogFun
|
|||
adapter_free(adapter);
|
||||
return 1;
|
||||
}
|
||||
stream = NULL;
|
||||
|
||||
// Create RemoteServerClient
|
||||
RemoteServerHandle *remote_server = NULL;
|
||||
|
|
@ -237,8 +240,10 @@ int debug_app_pid(IdeviceProviderHandle* tcp_provider, int pid, LogFuncC logger,
|
|||
fprintf(stderr, "Failed to create TCP adapter: [%d] %s\n", err->code,
|
||||
err->message);
|
||||
idevice_error_free(err);
|
||||
core_device_proxy_free(core_device);
|
||||
return 1;
|
||||
}
|
||||
core_device = NULL;
|
||||
|
||||
AdapterStreamHandle *stream = NULL;
|
||||
err = adapter_connect(adapter, rsd_port, (ReadWriteOpaque **)&stream);
|
||||
|
|
@ -263,6 +268,7 @@ int debug_app_pid(IdeviceProviderHandle* tcp_provider, int pid, LogFuncC logger,
|
|||
adapter_free(adapter);
|
||||
return 1;
|
||||
}
|
||||
stream = NULL;
|
||||
|
||||
// Create RemoteServerClient
|
||||
RemoteServerHandle *remote_server = NULL;
|
||||
|
|
@ -308,76 +314,66 @@ int launch_app_via_proxy(IdeviceProviderHandle* tcp_provider, const char *bundle
|
|||
IdeviceFfiError* err = NULL;
|
||||
|
||||
CoreDeviceProxyHandle *core_device = NULL;
|
||||
AdapterHandle *adapter = NULL;
|
||||
AdapterStreamHandle *stream = NULL;
|
||||
RsdHandshakeHandle *handshake = NULL;
|
||||
RemoteServerHandle *remote_server = NULL;
|
||||
ProcessControlHandle *process_control = NULL;
|
||||
uint64_t pid = 0;
|
||||
int result = 1;
|
||||
|
||||
err = core_device_proxy_connect(tcp_provider, &core_device);
|
||||
if (err != NULL) {
|
||||
fprintf(stderr, "Failed to connect to CoreDeviceProxy: [%d] %s\n", err->code, err->message);
|
||||
idevice_error_free(err);
|
||||
return 1;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
uint16_t rsd_port;
|
||||
uint16_t rsd_port = 0;
|
||||
err = core_device_proxy_get_server_rsd_port(core_device, &rsd_port);
|
||||
if (err != NULL) {
|
||||
fprintf(stderr, "Failed to get server RSD port: [%d] %s\n", err->code, err->message);
|
||||
idevice_error_free(err);
|
||||
core_device_proxy_free(core_device);
|
||||
return 1;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
AdapterHandle *adapter = NULL;
|
||||
err = core_device_proxy_create_tcp_adapter(core_device, &adapter);
|
||||
if (err != NULL) {
|
||||
fprintf(stderr, "Failed to create TCP adapter: [%d] %s\n", err->code, err->message);
|
||||
idevice_error_free(err);
|
||||
core_device_proxy_free(core_device);
|
||||
return 1;
|
||||
goto cleanup;
|
||||
}
|
||||
core_device = NULL; // ownership transferred to adapter
|
||||
|
||||
AdapterStreamHandle *stream = NULL;
|
||||
err = adapter_connect(adapter, rsd_port, (ReadWriteOpaque **)&stream);
|
||||
if (err != NULL) {
|
||||
fprintf(stderr, "Failed to connect to RSD port: [%d] %s\n", err->code, err->message);
|
||||
idevice_error_free(err);
|
||||
adapter_free(adapter);
|
||||
core_device_proxy_free(core_device);
|
||||
return 1;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
RsdHandshakeHandle *handshake = NULL;
|
||||
err = rsd_handshake_new((ReadWriteOpaque *)stream, &handshake);
|
||||
if (err != NULL) {
|
||||
fprintf(stderr, "Failed to perform RSD handshake: [%d] %s\n", err->code, err->message);
|
||||
idevice_error_free(err);
|
||||
adapter_close(stream);
|
||||
adapter_free(adapter);
|
||||
core_device_proxy_free(core_device);
|
||||
return 1;
|
||||
goto cleanup;
|
||||
}
|
||||
stream = NULL; // consumed by handshake/adapter stack
|
||||
|
||||
RemoteServerHandle *remote_server = NULL;
|
||||
err = remote_server_connect_rsd(adapter, handshake, &remote_server);
|
||||
if (err != NULL) {
|
||||
fprintf(stderr, "Failed to create remote server: [%d] %s\n", err->code, err->message);
|
||||
idevice_error_free(err);
|
||||
rsd_handshake_free(handshake);
|
||||
adapter_free(adapter);
|
||||
core_device_proxy_free(core_device);
|
||||
return 1;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
ProcessControlHandle *process_control = NULL;
|
||||
err = process_control_new(remote_server, &process_control);
|
||||
if (err != NULL) {
|
||||
fprintf(stderr, "Failed to create process control client: [%d] %s\n", err->code, err->message);
|
||||
idevice_error_free(err);
|
||||
remote_server_free(remote_server);
|
||||
rsd_handshake_free(handshake);
|
||||
adapter_free(adapter);
|
||||
core_device_proxy_free(core_device);
|
||||
return 1;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
uint64_t pid = 0;
|
||||
err = process_control_launch_app(process_control,
|
||||
bundle_id,
|
||||
NULL,
|
||||
|
|
@ -390,26 +386,37 @@ int launch_app_via_proxy(IdeviceProviderHandle* tcp_provider, const char *bundle
|
|||
if (err != NULL) {
|
||||
fprintf(stderr, "Failed to launch app: [%d] %s\n", err->code, err->message);
|
||||
idevice_error_free(err);
|
||||
process_control_free(process_control);
|
||||
remote_server_free(remote_server);
|
||||
rsd_handshake_free(handshake);
|
||||
adapter_free(adapter);
|
||||
core_device_proxy_free(core_device);
|
||||
if (logger) {
|
||||
logger("Failed to launch app: %s", bundle_id);
|
||||
}
|
||||
return 1;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (logger) {
|
||||
logger("Launched app (PID %llu)", pid);
|
||||
}
|
||||
|
||||
process_control_free(process_control);
|
||||
remote_server_free(remote_server);
|
||||
rsd_handshake_free(handshake);
|
||||
adapter_free(adapter);
|
||||
core_device_proxy_free(core_device);
|
||||
result = 0;
|
||||
|
||||
return 0;
|
||||
cleanup:
|
||||
if (process_control) {
|
||||
process_control_free(process_control);
|
||||
}
|
||||
if (remote_server) {
|
||||
remote_server_free(remote_server);
|
||||
}
|
||||
if (handshake) {
|
||||
rsd_handshake_free(handshake);
|
||||
}
|
||||
if (stream) {
|
||||
adapter_close(stream);
|
||||
}
|
||||
if (adapter) {
|
||||
adapter_free(adapter);
|
||||
}
|
||||
if (core_device) {
|
||||
core_device_proxy_free(core_device);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue