StikDebug/StikDebug/App/AppBootstrapper.swift
Claude 84a5a5eff6
Add experimental "hold app alive in background" mode
Targets the real problem: an app (e.g. Roblox) getting closed/disconnected by
iOS once it's in the background. iOS won't let one app keep another alive —
except that a process a debugger is attached to is not suspended on
backgrounding (the same reason an Xcode-run app keeps executing in the
background). This uses that.

- JITEnableContext.keepAppAlive: launch the app suspended, vAttach (which sets
  CS_DEBUGGED so JIT is on and marks the process debugger-owned), disable its
  memory limit to reduce background jetsam kills, send a raw continue so it
  runs, then hold the debug connection open with a heartbeat until the user
  stops it — then interrupt + detach so the app keeps running on its own. The
  hold is a single-threaded sleep-poll (no blocking socket reads, no concurrent
  handle access), so it's cleanly cancellable.
- BackgroundAliveManager + HoldToken: own one active hold, keep StikDebug itself
  alive via DebugKeepAliveLease for the duration, publish the active app name.
- UI: an experimental Settings toggle ("Hold App Alive in Background"); when on,
  tapping an app in Home holds it alive instead of a normal one-shot JIT, and a
  banner with a Stop button appears while a hold is active.

Caveats (documented in-app): experimental and unverified on-device; requires
StikDebug to stay running, so it's best-effort not bulletproof; cannot revive
an app iOS already killed; higher battery use.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qznh32XffG6vwFLMcwm1Mq
2026-07-14 12:53:28 +00:00

50 lines
1.6 KiB
Swift

//
// AppBootstrapper.swift
// StikDebug
//
import Foundation
import ObjectiveC.runtime
import UIKit
enum AppBootstrapper {
static func configure() {
registerDefaultSettings()
startConfiguredKeepAliveServices()
applyDocumentPickerCopyWorkaround()
NetworkPathMonitor.shared.start()
}
private static func registerDefaultSettings() {
let os = ProcessInfo.processInfo.operatingSystemVersion
let enableAdvancedOptions = os.majorVersion >= 19
UserDefaults.standard.register(defaults: [
"enableAdvancedOptions": enableAdvancedOptions,
UserDefaults.Keys.txmOverride: false,
UserDefaults.Keys.confirmExternalJITRequests: true,
"keepAliveAudio": true,
"keepAliveLocation": true,
"keepAppAliveBackground": false
])
}
private static func startConfiguredKeepAliveServices() {
guard UserDefaults.standard.bool(forKey: "keepAliveAudio") else {
return
}
BackgroundAudioManager.shared.start()
}
private static func applyDocumentPickerCopyWorkaround() {
let fixedSelector = NSSelectorFromString("fix_initForOpeningContentTypes:asCopy:")
let originalSelector = #selector(UIDocumentPickerViewController.init(forOpeningContentTypes:asCopy:))
guard let fixedMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, fixedSelector),
let originalMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, originalSelector) else {
return
}
method_exchangeImplementations(originalMethod, fixedMethod)
}
}