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
|
/// Launches an app, attaches the debugger, runs the app's assigned JIT
|
||||||
/// app keeps running in the background.
|
/// 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
|
/// 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
|
/// 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
|
/// background jetsam kill. The hold runs until `cancellation` is set, then we
|
||||||
/// interrupt and detach so the app continues running on its own.
|
/// 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
|
/// - Note: This keeps StikDebug's connection to *this* app alive; it cannot
|
||||||
/// revive an app iOS has already killed. Requires StikDebug itself to stay
|
/// revive an app iOS has already killed. Requires StikDebug itself to stay
|
||||||
/// running (see `DebugKeepAliveLease`).
|
/// 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 {
|
do {
|
||||||
try withConnectedDebugSession { remoteServer, debugProxy in
|
try withConnectedDebugSession { remoteServer, debugProxy in
|
||||||
let pid = try withProcessControl(remoteServer: remoteServer) { processControl in
|
let pid = try withProcessControl(remoteServer: remoteServer) { processControl in
|
||||||
|
|
@ -677,7 +682,14 @@ final class JITEnableContext {
|
||||||
return Int32(truncatingIfNeeded: pid)
|
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)
|
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)
|
||||||
debug_proxy_send_ack(debugProxy)
|
debug_proxy_send_ack(debugProxy)
|
||||||
|
|
||||||
|
|
@ -701,23 +720,47 @@ final class JITEnableContext {
|
||||||
|
|
||||||
debug_proxy_set_ack_mode(debugProxy, 0)
|
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.
|
// Keep the RSD/lockdown tunnel trusted for the whole hold.
|
||||||
let heartbeat = try? connectHeartbeatKeepAlive(logger: logger)
|
let heartbeat = try? connectHeartbeatKeepAlive(logger: logger)
|
||||||
try? heartbeat?.start()
|
try? heartbeat?.start()
|
||||||
defer { heartbeat?.stop() }
|
defer { heartbeat?.stop() }
|
||||||
|
|
||||||
// Continue the (suspended-launched) process so it runs. Sent raw and not
|
if let script {
|
||||||
// awaited: a debugger-owned, running process is not stopped by iOS when
|
// TXM / iOS 26+ JIT: the app requests executable memory at runtime via
|
||||||
// backgrounded, so there are no stop replies to drain — we just hold the
|
// `brk #0xf00d` syscalls that the debugger must service. That is exactly
|
||||||
// connection open. Polling a flag (rather than blocking on a socket read)
|
// what the JIT script does — it attaches (`vAttach`, enabling JIT),
|
||||||
// keeps this single-threaded and cleanly cancellable.
|
// 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)
|
sendContinue(debugProxy)
|
||||||
emitLog("Keep-alive: app is running and held under the debugger", logger: logger)
|
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 {
|
while !cancellation.isCancelled {
|
||||||
Thread.sleep(forTimeInterval: 0.5)
|
Thread.sleep(forTimeInterval: 0.5)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,10 @@ final class BackgroundAliveManager: ObservableObject {
|
||||||
return activeBundleID != nil
|
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()
|
lock.lock()
|
||||||
guard activeBundleID == nil else {
|
guard activeBundleID == nil else {
|
||||||
lock.unlock()
|
lock.unlock()
|
||||||
|
|
@ -76,6 +79,7 @@ final class BackgroundAliveManager: ObservableObject {
|
||||||
|
|
||||||
let succeeded = JITEnableContext.shared.keepAppAlive(
|
let succeeded = JITEnableContext.shared.keepAppAlive(
|
||||||
withBundleID: bundleID,
|
withBundleID: bundleID,
|
||||||
|
script: script,
|
||||||
cancellation: token,
|
cancellation: token,
|
||||||
logger: logger
|
logger: logger
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -291,9 +291,17 @@ struct HomeView: View {
|
||||||
)
|
)
|
||||||
return
|
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)
|
let startingMessage = String(format: "Keeping %@ alive in the background".localized, name)
|
||||||
AccessibilityAnnouncer.announce(startingMessage)
|
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 {
|
private func debugFeedbackView(_ feedback: DebugFeedback) -> some View {
|
||||||
|
|
|
||||||
|
|
@ -1436,6 +1436,7 @@ struct LocationSimulationView: View {
|
||||||
@State private var routePolyline: MKPolyline?
|
@State private var routePolyline: MKPolyline?
|
||||||
@State private var routePlaybackSamples: [RoutePlaybackSample] = []
|
@State private var routePlaybackSamples: [RoutePlaybackSample] = []
|
||||||
@State private var routeBusStops: [CLLocationCoordinate2D] = []
|
@State private var routeBusStops: [CLLocationCoordinate2D] = []
|
||||||
|
@State private var visibleLatitudeDelta: CLLocationDegrees = 0.05
|
||||||
@State private var routePlaybackCoordinate: CLLocationCoordinate2D?
|
@State private var routePlaybackCoordinate: CLLocationCoordinate2D?
|
||||||
@State private var simulatedCoordinate: CLLocationCoordinate2D?
|
@State private var simulatedCoordinate: CLLocationCoordinate2D?
|
||||||
@State private var routeRequestID = UUID()
|
@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 {
|
private var speedProfile: SpeedProfile {
|
||||||
SpeedProfile(rawValue: speedProfileRawValue) ?? .driving
|
SpeedProfile(rawValue: speedProfileRawValue) ?? .driving
|
||||||
}
|
}
|
||||||
|
|
@ -1648,11 +1672,9 @@ struct LocationSimulationView: View {
|
||||||
MapPolyline(routePolyline)
|
MapPolyline(routePolyline)
|
||||||
.stroke(.blue.opacity(0.8), lineWidth: 5)
|
.stroke(.blue.opacity(0.8), lineWidth: 5)
|
||||||
}
|
}
|
||||||
if speedProfile == .bus {
|
ForEach(Array(displayedBusStops.enumerated()), id: \.offset) { _, stop in
|
||||||
ForEach(Array(routeBusStops.enumerated()), id: \.offset) { _, stop in
|
Marker("Bus Stop", systemImage: "bus.fill", coordinate: stop)
|
||||||
Marker("Bus Stop", systemImage: "bus.fill", coordinate: stop)
|
.tint(.orange)
|
||||||
.tint(.orange)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if let routeStartCoordinate {
|
if let routeStartCoordinate {
|
||||||
Marker("Start", coordinate: routeStartCoordinate)
|
Marker("Start", coordinate: routeStartCoordinate)
|
||||||
|
|
@ -1680,6 +1702,9 @@ struct LocationSimulationView: View {
|
||||||
.mapControls {
|
.mapControls {
|
||||||
MapCompass()
|
MapCompass()
|
||||||
}
|
}
|
||||||
|
.onMapCameraChange(frequency: .onEnd) { context in
|
||||||
|
visibleLatitudeDelta = context.region.span.latitudeDelta
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
.onChange(of: coordinate.map(CoordinateSnapshot.init)) { _, new in
|
.onChange(of: coordinate.map(CoordinateSnapshot.init)) { _, new in
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue