mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-15 20:10:30 +00:00
Merge pull request #6 from dizzafizza/claude/fork-code-review-33fkom
Honor "Hidden" points of interest for bus stops and declutter at low zoom, fix JIT scripts
This commit is contained in:
commit
5c3c37a1e4
4 changed files with 101 additions and 21 deletions
|
|
@ -642,8 +642,9 @@ final class JITEnableContext {
|
|||
}
|
||||
}
|
||||
|
||||
/// Launches an app, attaches the debugger, and *holds* the session open so the
|
||||
/// app keeps running in the background.
|
||||
/// Launches an app, attaches the debugger, runs the app's assigned JIT
|
||||
/// script (if any), and *holds* the session open so the app keeps running
|
||||
/// in the background.
|
||||
///
|
||||
/// A debugger-owned process is not suspended by iOS when it moves to the
|
||||
/// background (the same reason an app run from Xcode keeps executing while
|
||||
|
|
@ -652,10 +653,14 @@ final class JITEnableContext {
|
|||
/// background jetsam kill. The hold runs until `cancellation` is set, then we
|
||||
/// interrupt and detach so the app continues running on its own.
|
||||
///
|
||||
/// - Parameter script: The same JIT-script callback the normal debug path
|
||||
/// uses. When present it runs once (doing its own attach + JIT enable +
|
||||
/// continue) before the hold begins, so scripts work in hold mode too;
|
||||
/// when `nil` the app is simply attached and continued.
|
||||
/// - Note: This keeps StikDebug's connection to *this* app alive; it cannot
|
||||
/// revive an app iOS has already killed. Requires StikDebug itself to stay
|
||||
/// running (see `DebugKeepAliveLease`).
|
||||
func keepAppAlive(withBundleID bundleID: String, cancellation: HoldToken, logger: LogFunc?) -> Bool {
|
||||
func keepAppAlive(withBundleID bundleID: String, script: DebugAppCallback?, cancellation: HoldToken, logger: LogFunc?) -> Bool {
|
||||
do {
|
||||
try withConnectedDebugSession { remoteServer, debugProxy in
|
||||
let pid = try withProcessControl(remoteServer: remoteServer) { processControl in
|
||||
|
|
@ -677,7 +682,14 @@ final class JITEnableContext {
|
|||
return Int32(truncatingIfNeeded: pid)
|
||||
}
|
||||
|
||||
try holdDebugSession(pid: pid, debugProxy: debugProxy, logger: logger, cancellation: cancellation)
|
||||
try holdDebugSession(
|
||||
pid: pid,
|
||||
debugProxy: debugProxy,
|
||||
remoteServer: remoteServer,
|
||||
script: script,
|
||||
logger: logger,
|
||||
cancellation: cancellation
|
||||
)
|
||||
}
|
||||
|
||||
emitLog("Keep-alive session ended for \(bundleID)", logger: logger)
|
||||
|
|
@ -688,7 +700,14 @@ final class JITEnableContext {
|
|||
}
|
||||
}
|
||||
|
||||
private func holdDebugSession(pid: Int32, debugProxy: OpaquePointer, logger: LogFunc?, cancellation: HoldToken) throws {
|
||||
private func holdDebugSession(
|
||||
pid: Int32,
|
||||
debugProxy: OpaquePointer,
|
||||
remoteServer: OpaquePointer,
|
||||
script: DebugAppCallback?,
|
||||
logger: LogFunc?,
|
||||
cancellation: HoldToken
|
||||
) throws {
|
||||
debug_proxy_send_ack(debugProxy)
|
||||
debug_proxy_send_ack(debugProxy)
|
||||
|
||||
|
|
@ -701,23 +720,47 @@ final class JITEnableContext {
|
|||
|
||||
debug_proxy_set_ack_mode(debugProxy, 0)
|
||||
|
||||
let attachCommand = "vAttach;\(String(UInt32(bitPattern: pid), radix: 16))"
|
||||
let attachResponse = try sendDebugCommand(attachCommand, debugProxy: debugProxy) ?? "<nil>"
|
||||
emitLog("Keep-alive attach response: \(attachResponse)", logger: logger)
|
||||
|
||||
// Keep the RSD/lockdown tunnel trusted for the whole hold.
|
||||
let heartbeat = try? connectHeartbeatKeepAlive(logger: logger)
|
||||
try? heartbeat?.start()
|
||||
defer { heartbeat?.stop() }
|
||||
|
||||
// Continue the (suspended-launched) process so it runs. Sent raw and not
|
||||
// awaited: a debugger-owned, running process is not stopped by iOS when
|
||||
// backgrounded, so there are no stop replies to drain — we just hold the
|
||||
// connection open. Polling a flag (rather than blocking on a socket read)
|
||||
// keeps this single-threaded and cleanly cancellable.
|
||||
if let script {
|
||||
// TXM / iOS 26+ JIT: the app requests executable memory at runtime via
|
||||
// `brk #0xf00d` syscalls that the debugger must service. That is exactly
|
||||
// what the JIT script does — it attaches (`vAttach`, enabling JIT),
|
||||
// then loops handling those breakpoints for as long as the app uses
|
||||
// JIT, which also keeps the app alive under the debugger the whole
|
||||
// time. Merely attaching without running the script (the old hold
|
||||
// behavior) left those syscalls unhandled, so the app trapped and hung
|
||||
// on its first JIT call — the bug this fixes.
|
||||
//
|
||||
// The script issues its own attach/continue via `send_command`, so we
|
||||
// must NOT send `vAttach` here (that would double-attach). It runs
|
||||
// until the app detaches or exits; the semaphore blocks until then.
|
||||
// StikDebug itself is held alive across this by the caller's
|
||||
// `DebugKeepAliveLease`.
|
||||
emitLog("Keep-alive: running JIT script (services JIT and holds the app)", logger: logger)
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
script(pid, debugProxy, remoteServer, semaphore)
|
||||
semaphore.wait()
|
||||
emitLog("Keep-alive: JIT script ended (app detached or exited)", logger: logger)
|
||||
return
|
||||
}
|
||||
|
||||
// No script (older devices where JIT is enabled simply by CS_DEBUGGED):
|
||||
// attach to set CS_DEBUGGED, then continue so the app runs. Sent raw and
|
||||
// not awaited: a debugger-owned, running process is not stopped by iOS
|
||||
// when backgrounded, so there are no stop replies to drain — we just hold
|
||||
// the connection open until the user stops it.
|
||||
let attachCommand = "vAttach;\(String(UInt32(bitPattern: pid), radix: 16))"
|
||||
let attachResponse = try sendDebugCommand(attachCommand, debugProxy: debugProxy) ?? "<nil>"
|
||||
emitLog("Keep-alive attach response: \(attachResponse)", logger: logger)
|
||||
sendContinue(debugProxy)
|
||||
emitLog("Keep-alive: app is running and held under the debugger", logger: logger)
|
||||
|
||||
// Polling a flag (rather than blocking on a socket read) keeps this
|
||||
// single-threaded and cleanly cancellable.
|
||||
while !cancellation.isCancelled {
|
||||
Thread.sleep(forTimeInterval: 0.5)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,10 @@ final class BackgroundAliveManager: ObservableObject {
|
|||
return activeBundleID != nil
|
||||
}
|
||||
|
||||
func start(bundleID: String, displayName: String?) {
|
||||
/// - Parameter script: Optional JIT-script callback to run once (after
|
||||
/// attach, before the hold begins) so the app's assigned script executes
|
||||
/// in hold mode just like it does for a normal JIT run.
|
||||
func start(bundleID: String, displayName: String?, script: DebugAppCallback? = nil) {
|
||||
lock.lock()
|
||||
guard activeBundleID == nil else {
|
||||
lock.unlock()
|
||||
|
|
@ -76,6 +79,7 @@ final class BackgroundAliveManager: ObservableObject {
|
|||
|
||||
let succeeded = JITEnableContext.shared.keepAppAlive(
|
||||
withBundleID: bundleID,
|
||||
script: script,
|
||||
cancellation: token,
|
||||
logger: logger
|
||||
)
|
||||
|
|
|
|||
|
|
@ -291,9 +291,17 @@ struct HomeView: View {
|
|||
)
|
||||
return
|
||||
}
|
||||
// Resolve the app's assigned JIT script the same way a normal JIT run
|
||||
// does, so the script also runs in hold mode instead of only attaching.
|
||||
var script: DebugAppCallback? = nil
|
||||
if ProcessInfo.processInfo.hasTXM,
|
||||
let preferred = ScriptStore.preferredScript(for: bundleID) {
|
||||
script = getJsCallback(preferred.data, name: preferred.name)
|
||||
}
|
||||
|
||||
let startingMessage = String(format: "Keeping %@ alive in the background".localized, name)
|
||||
AccessibilityAnnouncer.announce(startingMessage)
|
||||
BackgroundAliveManager.shared.start(bundleID: bundleID, displayName: displayName)
|
||||
BackgroundAliveManager.shared.start(bundleID: bundleID, displayName: displayName, script: script)
|
||||
}
|
||||
|
||||
private func debugFeedbackView(_ feedback: DebugFeedback) -> some View {
|
||||
|
|
|
|||
|
|
@ -1436,6 +1436,7 @@ struct LocationSimulationView: View {
|
|||
@State private var routePolyline: MKPolyline?
|
||||
@State private var routePlaybackSamples: [RoutePlaybackSample] = []
|
||||
@State private var routeBusStops: [CLLocationCoordinate2D] = []
|
||||
@State private var visibleLatitudeDelta: CLLocationDegrees = 0.05
|
||||
@State private var routePlaybackCoordinate: CLLocationCoordinate2D?
|
||||
@State private var simulatedCoordinate: CLLocationCoordinate2D?
|
||||
@State private var routeRequestID = UUID()
|
||||
|
|
@ -1494,6 +1495,29 @@ struct LocationSimulationView: View {
|
|||
}
|
||||
}
|
||||
|
||||
/// Bus stop markers to actually draw on the map. Two adjustments on top of
|
||||
/// the raw fetched `routeBusStops`:
|
||||
///
|
||||
/// - Honors "Points of Interest: Hidden" — the stops are drawn as our own
|
||||
/// Marker overlay, not Apple's native POI layer, so Apple's
|
||||
/// `pointsOfInterest` style option (set from the same picker) has no
|
||||
/// effect on them; that had to be handled here explicitly.
|
||||
/// - Thins markers as the map zooms out, since a bus route easily has
|
||||
/// dozens of stops that overlap into an unreadable stack of pins at a
|
||||
/// city-wide zoom. Spacing scales with the visible latitude span so
|
||||
/// roughly the same number of markers are visible on screen at any
|
||||
/// zoom level; the full-fidelity `routeBusStops` list (used for dwell
|
||||
/// pacing during playback) is untouched.
|
||||
private var displayedBusStops: [CLLocationCoordinate2D] {
|
||||
guard speedProfile == .bus, mapPointsOfInterestMode != .hidden else { return [] }
|
||||
let visibleSpanMeters = visibleLatitudeDelta * 111_320
|
||||
let minimumSpacing = max(
|
||||
SpeedProfileSettings.busStopSnapRadius,
|
||||
visibleSpanMeters / 25
|
||||
)
|
||||
return dedupedBusStops(routeBusStops, radius: minimumSpacing)
|
||||
}
|
||||
|
||||
private var speedProfile: SpeedProfile {
|
||||
SpeedProfile(rawValue: speedProfileRawValue) ?? .driving
|
||||
}
|
||||
|
|
@ -1648,12 +1672,10 @@ struct LocationSimulationView: View {
|
|||
MapPolyline(routePolyline)
|
||||
.stroke(.blue.opacity(0.8), lineWidth: 5)
|
||||
}
|
||||
if speedProfile == .bus {
|
||||
ForEach(Array(routeBusStops.enumerated()), id: \.offset) { _, stop in
|
||||
ForEach(Array(displayedBusStops.enumerated()), id: \.offset) { _, stop in
|
||||
Marker("Bus Stop", systemImage: "bus.fill", coordinate: stop)
|
||||
.tint(.orange)
|
||||
}
|
||||
}
|
||||
if let routeStartCoordinate {
|
||||
Marker("Start", coordinate: routeStartCoordinate)
|
||||
.tint(.green)
|
||||
|
|
@ -1680,6 +1702,9 @@ struct LocationSimulationView: View {
|
|||
.mapControls {
|
||||
MapCompass()
|
||||
}
|
||||
.onMapCameraChange(frequency: .onEnd) { context in
|
||||
visibleLatitudeDelta = context.region.span.latitudeDelta
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.onChange(of: coordinate.map(CoordinateSnapshot.init)) { _, new in
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue