mobile-debug-mcp 0.35.0 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor-plugin/marketplace.json +1 -1
- package/.github/workflows/ci.yml +1 -1
- package/README.md +86 -24
- package/agent-plugin/.antigravity-plugin/README.md +48 -0
- package/agent-plugin/.antigravity-plugin/plugin.json +18 -0
- package/agent-plugin/.claude-plugin/plugin.json +2 -2
- package/agent-plugin/.codex-plugin/plugin.json +1 -1
- package/agent-plugin/.cursor-plugin/plugin.json +2 -2
- package/agent-plugin/.mcp.json +1 -1
- package/agent-plugin/README.md +37 -8
- package/agent-plugin/cursor/.cursor-plugin/plugin.json +23 -0
- package/agent-plugin/cursor/LICENSE +201 -0
- package/agent-plugin/cursor/README.md +9 -0
- package/agent-plugin/cursor/skills/mobile-debug-mcp/SKILL.md +253 -0
- package/agent-plugin/mcp.json +1 -1
- package/agent-plugin/mcp_config.json +13 -0
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +36 -16
- package/dist/debugger/guard.js +4 -4
- package/dist/debugger/ios-worker.js +62 -0
- package/dist/debugger/ios.js +57 -0
- package/dist/debugger/sessions.js +33 -16
- package/dist/debugger/worker.js +6 -4
- package/dist/interact/index.js +12 -2
- package/dist/manage/ios.js +2 -0
- package/dist/observe/ios-trace/artifact.js +63 -0
- package/dist/observe/ios-trace/capabilities.js +86 -0
- package/dist/observe/ios-trace/native.js +93 -0
- package/dist/observe/ios-trace/notification.js +35 -0
- package/dist/observe/ios-trace/process.js +98 -0
- package/dist/observe/ios-trace/sessions.js +244 -0
- package/dist/observe/readiness-service.js +1 -1
- package/dist/observe/readiness.js +4 -4
- package/dist/observe/system-trace-error.js +7 -0
- package/dist/observe/system-trace-router.js +81 -0
- package/dist/observe/system-trace.js +4 -8
- package/dist/server/handlers/debugger-handlers.js +27 -8
- package/dist/server/handlers/diagnostic.js +12 -4
- package/dist/server/handlers/tool-handlers.js +19 -9
- package/dist/server/schemas/debugger-schema.js +10 -7
- package/dist/server/schemas/system-trace-schema.js +14 -9
- package/dist/server-core.js +1 -1
- package/dist/server.js +4 -4
- package/dist/utils/ios/utils.js +3 -0
- package/docs/CHANGELOG.md +23 -1
- package/docs/agent-plugin-installation-verification.md +39 -0
- package/docs/code-structure.md +3 -1
- package/docs/specs/031-ios-system-traces/intent.md +38 -0
- package/docs/specs/031-ios-system-traces/investigation.md +92 -0
- package/docs/specs/031-ios-system-traces/late-run-failure-evidence.json +32 -0
- package/docs/specs/031-ios-system-traces/plan.md +138 -0
- package/docs/specs/031-ios-system-traces/pr102-monitor-evidence.json +22 -0
- package/docs/specs/031-ios-system-traces/simulator-evidence.json +47 -0
- package/docs/specs/031-ios-system-traces/spec.md +135 -0
- package/docs/specs/032-ios-runtime-debugger/evidence.json +176 -0
- package/docs/specs/032-ios-runtime-debugger/intent.md +38 -0
- package/docs/specs/032-ios-runtime-debugger/plan.md +95 -0
- package/docs/specs/032-ios-runtime-debugger/report.md +205 -0
- package/docs/specs/032-ios-runtime-debugger/spec.md +153 -0
- package/docs/tools/TOOLS.md +2 -2
- package/docs/tools/debugger.md +45 -5
- package/docs/tools/system-traces.md +81 -14
- package/package.json +2 -1
- package/scripts/agent-plugin.mjs +55 -5
- package/src/debugger/android.ts +1 -1
- package/src/debugger/guard.ts +5 -5
- package/src/debugger/ios-worker.ts +49 -0
- package/src/debugger/ios.ts +45 -0
- package/src/debugger/lldb/breakpoints.py +51 -0
- package/src/debugger/lldb/inspection.py +109 -0
- package/src/debugger/lldb/worker.py +203 -0
- package/src/debugger/sessions.ts +24 -12
- package/src/debugger/worker.ts +7 -4
- package/src/interact/index.ts +13 -2
- package/src/manage/ios.ts +2 -0
- package/src/observe/ios-trace/artifact.ts +55 -0
- package/src/observe/ios-trace/capabilities.ts +74 -0
- package/src/observe/ios-trace/native.ts +72 -0
- package/src/observe/ios-trace/notification.ts +30 -0
- package/src/observe/ios-trace/process.ts +82 -0
- package/src/observe/ios-trace/sessions.ts +204 -0
- package/src/observe/readiness-service.ts +1 -1
- package/src/observe/readiness.ts +4 -4
- package/src/observe/system-trace-error.ts +3 -0
- package/src/observe/system-trace-router.ts +61 -0
- package/src/observe/system-trace.ts +4 -4
- package/src/server/handlers/debugger-handlers.ts +28 -9
- package/src/server/handlers/diagnostic.ts +11 -4
- package/src/server/handlers/tool-handlers.ts +20 -9
- package/src/server/schemas/debugger-schema.ts +10 -7
- package/src/server/schemas/system-trace-schema.ts +15 -9
- package/src/server-core.ts +1 -1
- package/src/server.ts +3 -3
- package/src/utils/ios/utils.ts +3 -0
- package/test/device/automated/debugger/runtime.ios.test.ts +221 -0
- package/test/device/automated/observe/ios-system-trace.smoke.ts +119 -0
- package/test/fixtures/contracts/tool-definitions.json +192 -32
- package/test/fixtures/ios-debugger-app/Main.swift +95 -0
- package/test/fixtures/ios-debugger-app/README.md +27 -0
- package/test/fixtures/ios-debugger-app/build.ts +21 -0
- package/test/helpers/ios-trace-shutdown.ts +29 -0
- package/test/unit/debugger/handle-guards.test.ts +88 -0
- package/test/unit/debugger/ios-preparation.test.ts +57 -0
- package/test/unit/debugger/ios.test.ts +104 -0
- package/test/unit/debugger/lldb-worker.py +166 -0
- package/test/unit/debugger/lldb-worker.test.ts +10 -0
- package/test/unit/observe/ios-trace-native.test.ts +90 -0
- package/test/unit/observe/ios-trace-sessions.test.ts +131 -0
- package/test/unit/observe/readiness.test.ts +1 -1
- package/test/unit/observe/system-trace-router.test.ts +49 -0
- package/test/unit/quality/agent-plugin-export.test.ts +38 -0
- package/test/unit/server/system-trace.test.ts +12 -3
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# iOS runtime debugger — specification
|
|
2
|
+
|
|
3
|
+
Source: [issue #70](https://github.com/clivejefferies/mobile-debug-tools/issues/70).
|
|
4
|
+
Scope: [accepted intent](intent.md), approved by the user's “ok” on 2026-11-04.
|
|
5
|
+
Status: **approved** by the user's “ok” on 2026-11-04 in response to the complete
|
|
6
|
+
specification and approval request in this chat. Implementation authorised.
|
|
7
|
+
|
|
8
|
+
## Behaviour and supported environment
|
|
9
|
+
|
|
10
|
+
Add optional LLDB-backed debugging for an already-running development app on an
|
|
11
|
+
explicit iOS simulator UDID, on macOS with full Xcode and its Swift-enabled LLDB.
|
|
12
|
+
Initial verification targets the installed Xcode 27.2, LLDB 2103.0.34.407 and Swift
|
|
13
|
+
6.4.2 toolchain; other versions remain unverified until separately exercised.
|
|
14
|
+
Physical devices, remote hosts and production/optimised apps are unsupported in
|
|
15
|
+
this initial matrix. Normal mobile tools must not acquire an LLDB dependency.
|
|
16
|
+
|
|
17
|
+
Provide source breakpoints, stop events, bounded stacks and shallow locals,
|
|
18
|
+
into/over/out stepping, resume and detach through the existing eleven tools.
|
|
19
|
+
Attach may briefly stop the process for setup, but returns running unless an
|
|
20
|
+
actual stop event has been retained; never discard a racing breakpoint stop.
|
|
21
|
+
LLDB stops suspend the target process, not the whole simulator.
|
|
22
|
+
|
|
23
|
+
## Tool contract and compatibility
|
|
24
|
+
|
|
25
|
+
Extend `get_debugger_capabilities` and `attach_debugger` to accept
|
|
26
|
+
`platform: "android" | "ios"`, existing explicit `deviceId`, `appId` and optional
|
|
27
|
+
positive `processId`. For iOS, require a simulator UDID and valid bundle identifier.
|
|
28
|
+
Probe without attaching or launching the app. Report missing prerequisites,
|
|
29
|
+
unsupported target types, absent apps/processes and ambiguous candidates
|
|
30
|
+
explicitly. A probe cannot guarantee that later attach permission will succeed.
|
|
31
|
+
|
|
32
|
+
Preserve all existing Android requests. Extend `set_debugger_breakpoint` with
|
|
33
|
+
optional `sourceFile` (nonempty absolute local source path, maximum 512 characters).
|
|
34
|
+
Require exactly one of `className` or `sourceFile`, with the existing positive
|
|
35
|
+
1-based `line`. Android sessions require `className`; iOS sessions require
|
|
36
|
+
`sourceFile`. Invalid combinations fail `INVALID_ARGUMENT`. Resolve the full
|
|
37
|
+
path against loaded debug information, never a basename fallback or shell search.
|
|
38
|
+
Return owned breakpoint ID, pending/resolved/rejected status and actual locations.
|
|
39
|
+
Unloaded images can remain pending. Only exact executable line matches may be
|
|
40
|
+
enabled; locations silently moved by LLDB are rejected and removed.
|
|
41
|
+
|
|
42
|
+
Keep session, stop, thread and frame arguments, tool names, response modes,
|
|
43
|
+
pagination, defaults and timeout bounds unchanged. Existing common response and
|
|
44
|
+
error shapes remain: `success`, session/target/device metadata, state, timestamp,
|
|
45
|
+
capabilities, limitations, cleanup, reason, stop, operation `result`, and sanitized
|
|
46
|
+
`error: {code, message, retryable, diagnostic?}`. Device platform is correct on
|
|
47
|
+
success and failure. iOS target metadata carries bundle ID, PID, executable name,
|
|
48
|
+
process-start identity and nullable UID; Android retains its existing fields.
|
|
49
|
+
|
|
50
|
+
Capability values are backend-specific. iOS advertises supported core operations;
|
|
51
|
+
`evaluation`, `mutation`, `coroutines` and `native` remain false (`native` denotes
|
|
52
|
+
an exposed native-specific inspection capability, not LLDB's implementation).
|
|
53
|
+
Add iOS-only boolean `async_execution_inspection` and `async_stepping`; the former
|
|
54
|
+
requires the async acceptance journey below, the latter a separate demonstrated
|
|
55
|
+
step across suspension. False values carry explanatory limitations. No suspended
|
|
56
|
+
task enumeration, task identity API or logical-task reconstruction is promised.
|
|
57
|
+
Preserve compact mode's existing omissions and full attach/capabilities/state
|
|
58
|
+
responses. Unknown fields remain rejected.
|
|
59
|
+
|
|
60
|
+
## Async semantics
|
|
61
|
+
|
|
62
|
+
Source breakpoints must work before and after a controlled Swift `await`, with
|
|
63
|
+
useful known primitive locals at each actual stop. Include nested child work,
|
|
64
|
+
actor/executor transitions and a separately triggered task cancellation path.
|
|
65
|
+
Use deterministic fixture gates and completion/cancellation UI state.
|
|
66
|
+
|
|
67
|
+
Stepping acknowledges delivery and resumes; only a subsequent event establishes
|
|
68
|
+
where execution stopped. Exercise ordinary helper into/over/out and an `await`
|
|
69
|
+
boundary. Delegate stepping to Swift-enabled LLDB; do not equate a physical
|
|
70
|
+
thread ID with stable task identity. If the tested LLDB route follows the async
|
|
71
|
+
continuation, report the observed source stop and advertise `async_stepping`.
|
|
72
|
+
Otherwise report it false with the limitation and demonstrate recovery through
|
|
73
|
+
independent after-await breakpoints. Unexpected missing async locals or source
|
|
74
|
+
stops fail core async acceptance rather than being labelled support.
|
|
75
|
+
|
|
76
|
+
Stacks describe LLDB's observed frames; label unavailable source/variable data
|
|
77
|
+
and generated frames honestly. Do not claim a complete logical async ancestry.
|
|
78
|
+
No expression evaluation, object descriptions, custom formatters that execute
|
|
79
|
+
code, property getters, dynamic method calls or continuation-field traversal.
|
|
80
|
+
|
|
81
|
+
## Ownership, failure and safety
|
|
82
|
+
|
|
83
|
+
Resolve simulator, bundle executable and running PID together; validate executable
|
|
84
|
+
path, start identity and simulator membership before and after attach. Never attach
|
|
85
|
+
to an arbitrary host PID, launch a missing app or replace another debugger.
|
|
86
|
+
Reject identity changes and duplicate owned sessions. Serialize session mutations;
|
|
87
|
+
isolate sessions and guards by platform plus device, including identical IDs.
|
|
88
|
+
|
|
89
|
+
Reuse stop-scoped opaque references, the 256-event ring/100-event response limit,
|
|
90
|
+
1000-frame cap, default 20/max 100 stack page, max 100 locals and 256-character
|
|
91
|
+
string bound. Object locals expose type/opaque identity only. Missing or
|
|
92
|
+
inaccessible variables are explicit; an empty list cannot prove availability.
|
|
93
|
+
|
|
94
|
+
Generalise pause guards/readiness to iOS lifecycle actions, input, waits and
|
|
95
|
+
assertions. Preserve device-wide conservative guarding, racing-dispatch
|
|
96
|
+
uncertainty and observation access within existing deadlines. Do not claim
|
|
97
|
+
foreground identity from debugger state.
|
|
98
|
+
|
|
99
|
+
Use a dedicated owned LLDB worker with bounded structured IPC, argument-array
|
|
100
|
+
subprocess calls, disabled user init files and no arbitrary command interface.
|
|
101
|
+
Sanitize errors using the existing bounded diagnostic catalog; iOS recovery
|
|
102
|
+
guidance names Xcode/LLDB rather than JDK/ADB. Never log worker messages, locals,
|
|
103
|
+
raw stderr, exception text or source contents. Debug inspection stays local and
|
|
104
|
+
request-scoped; public evidence uses only the disposable fixture.
|
|
105
|
+
|
|
106
|
+
Retain 100–30000 ms request bounds, 60-second pause, five-minute idle and
|
|
107
|
+
30-minute session expiry, including independent worker watchdogs. Event-wait
|
|
108
|
+
cancellation preserves a usable session. Inspection timeouts preserve a known
|
|
109
|
+
session; uncertain attach or mutation outcomes trigger owned teardown and report
|
|
110
|
+
execution uncertainty. No automatic retries of attach/step/resume.
|
|
111
|
+
|
|
112
|
+
Detach releases owned suspension, breakpoints, step plans and worker resources;
|
|
113
|
+
never kill the app, another debugger or simulator. EOF/shutdown/expiry attempt
|
|
114
|
+
detach. Cleanup is idempotent and retryable with explicit remaining resources;
|
|
115
|
+
transport loss or forced host termination cannot imply confirmed recovery.
|
|
116
|
+
|
|
117
|
+
## Acceptance and verification
|
|
118
|
+
|
|
119
|
+
1. Real MCP simulator journey: attach, exact and pending source breakpoint,
|
|
120
|
+
stop, source stack, known locals, helper stepping, resume, removal and detach.
|
|
121
|
+
2. Async journey: before/after-await locals, nested work and executor transition,
|
|
122
|
+
completion, cancellation recovery, and recorded boundary-step outcome.
|
|
123
|
+
3. Safety: stale references, cross-session/device isolation, paused input and
|
|
124
|
+
readiness, cancelled event wait, process exit, attach failure, timeout,
|
|
125
|
+
automatic expiry and repeatable cleanup. Verify app usability after detach.
|
|
126
|
+
4. Contract checks cover registration, runtime validation, dispatch, both response
|
|
127
|
+
modes, diagnostics and Android compatibility. Run `npm run verify`, applicable
|
|
128
|
+
`npm run test:device`, focused iOS MCP journeys and Android debugger regressions.
|
|
129
|
+
5. Record fixture/server hashes, Xcode/LLDB/Swift/runtime versions, target UDID,
|
|
130
|
+
exact actions, observed values and independent UI-tree assertions. Report
|
|
131
|
+
physical-device and unavailable coverage as unrun, never inferred.
|
|
132
|
+
|
|
133
|
+
Likely affected areas: `src/debugger/`, debugger schemas/handlers/serialization,
|
|
134
|
+
iOS interaction/readiness boundaries, fixture/device/unit trees, packaging and
|
|
135
|
+
debugger tool documentation/README/changelog. Reuse worker/session abstractions
|
|
136
|
+
without routing Android through LLDB. Main risks are simulator process attribution,
|
|
137
|
+
LLDB/Python distribution, async metadata, source relocation and teardown races.
|
|
138
|
+
|
|
139
|
+
## Investigation evidence and remaining prerequisites
|
|
140
|
+
|
|
141
|
+
On 2026-11-04, local version probes found the toolchain above. Sandboxed simctl
|
|
142
|
+
could not contact CoreSimulator; an authorised outside-sandbox read succeeded and
|
|
143
|
+
found no booted simulators. No existing iOS debugger implementation or live iOS
|
|
144
|
+
debug journey is claimed. Implementation verification requires booting an
|
|
145
|
+
available simulator and installing only the disposable development fixture.
|
|
146
|
+
|
|
147
|
+
Apple documents async-aware stepping since Swift 6.2 in
|
|
148
|
+
[What's new in Swift](https://developer.apple.com/videos/play/wwdc2025/245/).
|
|
149
|
+
The [LLDB SBFrame API](https://lldb.llvm.org/python_reference/lldb.SBFrame-class.html)
|
|
150
|
+
provides frame-variable access. These support the proposed route, not proof of
|
|
151
|
+
this server's behaviour; exact worker loading, attach and safe variable extraction
|
|
152
|
+
must be validated before claiming support. If unavailable, record the concrete
|
|
153
|
+
blocker and return for a scope decision rather than weakening acceptance.
|
package/docs/tools/TOOLS.md
CHANGED
|
@@ -11,8 +11,8 @@ Use the guides below when you need a tool contract or a specific workflow.
|
|
|
11
11
|
- [Interaction](interact.md) — UI resolution, actions, waits, and deterministic verification.
|
|
12
12
|
- [Environment diagnostics](system.md) — toolchain discovery and health checks.
|
|
13
13
|
- [Evidence correlation](correlated-evidence.md) — action references, capture timing, and freshness.
|
|
14
|
-
- [
|
|
15
|
-
- [Android runtime debugger](debugger.md) — process attachment, breakpoints, and bounded runtime inspection.
|
|
14
|
+
- [System traces](system-traces.md) — Android Perfetto and iOS simulator Time Profiler capture with local artifacts.
|
|
15
|
+
- [Android and iOS simulator runtime debugger](debugger.md) — process attachment, breakpoints, and bounded runtime inspection.
|
|
16
16
|
|
|
17
17
|
## Interaction principles
|
|
18
18
|
|
package/docs/tools/debugger.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Runtime debugger
|
|
2
2
|
|
|
3
3
|
[Documentation](../README.md) · [Tool reference](TOOLS.md)
|
|
4
4
|
|
|
@@ -98,7 +98,7 @@ state-changing command. Timeout or cancellation does not prove nondelivery.
|
|
|
98
98
|
Retry incomplete cleanup with `detach_debugger`; never automatically retry
|
|
99
99
|
attach, step or resume, or kill the app/ADB server. Diagnostics improve failure
|
|
100
100
|
classification and do not establish the cause of a previously intermittent error.
|
|
101
|
-
|
|
101
|
+
These details describe Android; see the iOS simulator backend below for its prerequisites and recovery guidance.
|
|
102
102
|
|
|
103
103
|
## Suspension, limits and cleanup
|
|
104
104
|
|
|
@@ -108,8 +108,8 @@ racing input submission preserves delivery uncertainty and blocks verification.
|
|
|
108
108
|
Journeys preserve completed steps. Other devices are unaffected.
|
|
109
109
|
|
|
110
110
|
`get_app_readiness` returns evidence-backed `debugger_paused` without reading the
|
|
111
|
-
UI; readiness waits fail with `DEBUGGER_PAUSED`. This signal covers managed Android
|
|
112
|
-
sessions
|
|
111
|
+
UI; readiness waits fail with `DEBUGGER_PAUSED`. This signal covers managed Android and iOS simulator
|
|
112
|
+
sessions and does not establish foreground app identity. Raw trees, screenshots
|
|
113
113
|
and logs remain available within their own timeouts; snapshot/log acquisition may
|
|
114
114
|
still fail while the app is suspended. Known pauses are annotated on observations.
|
|
115
115
|
|
|
@@ -134,7 +134,7 @@ step, resume or attach.
|
|
|
134
134
|
## Limits, privacy and evidence
|
|
135
135
|
|
|
136
136
|
There is no expression evaluation, object traversal, method invocation, mutation,
|
|
137
|
-
coroutine reconstruction, native
|
|
137
|
+
coroutine reconstruction, native-specific inspection or heap capture. The optional iOS simulator backend is described below. Kotlin
|
|
138
138
|
results follow emitted bytecode/debug metadata; optimized/obfuscated apps may lack
|
|
139
139
|
useful source lines or locals. Locals may contain private data: values are returned
|
|
140
140
|
only on request, without protocol/value logging, persistent dumps or upload.
|
|
@@ -178,3 +178,43 @@ is selected. Fixture compilation requires SDK platform/build-tools 36 and `javac
|
|
|
178
178
|
- [App management](manage.md): build, install, and launch the app before attaching.
|
|
179
179
|
- [App readiness](observe.md#app-readiness): understand known debugger-pause evidence.
|
|
180
180
|
- [Android system traces](system-traces.md): investigate performance without paused-state inspection.
|
|
181
|
+
|
|
182
|
+
## iOS simulator backend
|
|
183
|
+
|
|
184
|
+
The same eleven tools accept `platform: "ios"` for target capability probes and
|
|
185
|
+
attach. Supply an explicit booted simulator UDID and bundle `appId`; an optional
|
|
186
|
+
`processId` must belong to that simulator's installed app. Requires macOS and full
|
|
187
|
+
Xcode with its Swift-enabled LLDB and Python bindings. Physical iOS devices remain
|
|
188
|
+
unsupported. Attach never launches an app, chooses a host process by name alone,
|
|
189
|
+
replaces another debugger, or changes security settings.
|
|
190
|
+
|
|
191
|
+
For `set_debugger_breakpoint`, supply `sourceFile` (absolute local source path)
|
|
192
|
+
and 1-based `line` for iOS; continue using binary `className` for Android. Exactly
|
|
193
|
+
one selector is required. iOS reports `resolved`, `pending` or `rejected` with
|
|
194
|
+
actual locations; LLDB line relocation is disabled. Pending breakpoints may bind
|
|
195
|
+
when their image loads. Other tool arguments, stop-scoped references, stack/locals
|
|
196
|
+
limits, event cursors, response modes and expiry durations follow the flow above.
|
|
197
|
+
|
|
198
|
+
iOS returns backend-specific capabilities and limitations; `target.uid` may be
|
|
199
|
+
null and `target.executable_path` identifies the verified simulator executable.
|
|
200
|
+
`async_execution_inspection` and `async_stepping` are independent capability flags.
|
|
201
|
+
False means the capability is not established for the backend's supported matrix;
|
|
202
|
+
inspect the limitations. A source breakpoint after await and a shallow local do
|
|
203
|
+
not enumerate suspended tasks or establish logical task identity. Step requests
|
|
204
|
+
acknowledge delivery; inspect the subsequent actual stop. Object locals expose
|
|
205
|
+
only bounded type/opaque identity; Swift standard numeric/bool wrappers are read
|
|
206
|
+
from static storage; Swift strings and other aggregates retain type summaries. No expressions, descriptions, dynamic getters or arbitrary
|
|
207
|
+
LLDB commands execute through this API.
|
|
208
|
+
|
|
209
|
+
Known iOS pauses guard lifecycle actions, input, waits and assertions on that
|
|
210
|
+
platform/device and supply `debugger_paused` readiness. Raw observations remain
|
|
211
|
+
available within their own deadlines. Detach removes owned breakpoints and worker
|
|
212
|
+
resources and releases owned suspension; it never kills the app or simulator.
|
|
213
|
+
A worker exit without confirmed detach/process exit cannot prove cleanup; retry
|
|
214
|
+
`detach_debugger` and inspect `cleanup.remaining_resources`. Failure diagnostics
|
|
215
|
+
provide static Xcode/LLDB-specific recovery guidance without raw worker text.
|
|
216
|
+
|
|
217
|
+
Use the [isolated simulator fixture](../../test/fixtures/ios-debugger-app/README.md)
|
|
218
|
+
for real MCP runtime and async checks. The [implementation plan](../specs/032-ios-runtime-debugger/plan.md)
|
|
219
|
+
records exact verification and unavailable coverage. An installed/published package,
|
|
220
|
+
physical device, or other Xcode version requires separate evidence.
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
# Android
|
|
1
|
+
# System traces for Android and iOS
|
|
2
2
|
|
|
3
3
|
[Documentation](../README.md) · [Tool reference](TOOLS.md)
|
|
4
4
|
|
|
5
5
|
Use system traces to collect scheduling and system annotations around a slow
|
|
6
|
-
interaction. Capture is optional
|
|
6
|
+
interaction. Capture is optional; Android uses device-wide Perfetto and iOS simulators use
|
|
7
|
+
app-attached Instruments Time Profiler. It does not
|
|
7
8
|
automatically diagnose performance problems. No root or Android CLI installation
|
|
8
9
|
is required. The device must provide Perfetto (Android API 29+), authorised ADB,
|
|
9
10
|
tracing services, required sched/input categories, readable process identity,
|
|
@@ -11,14 +12,14 @@ base64 decoding and writable `/data/misc/perfetto-traces` storage.
|
|
|
11
12
|
|
|
12
13
|
## Tools
|
|
13
14
|
|
|
14
|
-
All inputs reject unknown fields. `platform` is `android` or `ios
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
All inputs reject unknown fields. `platform` is `android` or `ios`. Android's
|
|
16
|
+
optional `deviceId` uses existing device resolution. iOS requires an explicit
|
|
17
|
+
booted simulator UDID, plus `appId` for start; it never records the Mac host.
|
|
17
18
|
|
|
18
19
|
| Tool | Arguments | Result |
|
|
19
20
|
| --- | --- | --- |
|
|
20
|
-
| `get_system_trace_capabilities` | `platform`, `deviceId?` | `supported`, `backend`, resolved `device`, prerequisite booleans,
|
|
21
|
-
| `start_system_trace` | `platform`, `deviceId?`, `durationMs?` | Owned recording session after
|
|
21
|
+
| `get_system_trace_capabilities` | `platform`, `deviceId?`, `appId?`, `template?` | Common: `supported`, `backend`, resolved `device`, reasons and limitations. Android: prerequisite booleans, `enabled_sources`, `unavailable_categories`. iOS: prerequisites as `available`/`missing`/`unknown`, `discovered_templates`, `templates`, `app`, `xcode_version`, `recovery`. No recording. |
|
|
22
|
+
| `start_system_trace` | `platform`, `deviceId?`, `durationMs?`, `appId?`, `template?` | Owned recording session after native readiness acknowledgement. Duration is an integer 1,000–120,000 ms, default 30,000. |
|
|
22
23
|
| `get_system_trace_status` | `traceId` | Reconcile process/deadline and finalise ended recording. |
|
|
23
24
|
| `stop_system_trace` | `traceId` | Gracefully stop and retrieve local artifact; retry transfer/cleanup after failure. |
|
|
24
25
|
| `cancel_system_trace` | `traceId` | Stop and discard partial files. Already retrieved artifacts remain local. |
|
|
@@ -26,7 +27,7 @@ resolution; supplying one is recommended when multiple devices are connected.
|
|
|
26
27
|
Trace control IDs are UUIDs and cannot redirect to another device. One capture
|
|
27
28
|
per device and four pending sessions per server are allowed. Pending failed
|
|
28
29
|
cleanup reserves its slot until resolved. Terminal records with complete cleanup
|
|
29
|
-
expire after 10 minutes or eviction beyond 100 records. Server restart invalidates
|
|
30
|
+
expire after 10 minutes or eviction beyond 100 records per backend (the router retains at most 100 completed IDs). Server restart invalidates
|
|
30
31
|
IDs. Repeated terminal operations return the stored outcome.
|
|
31
32
|
|
|
32
33
|
## Capture workflow
|
|
@@ -89,12 +90,13 @@ proof of causality; host intervals are not aligned with device trace clocks.
|
|
|
89
90
|
## Session response and failures
|
|
90
91
|
|
|
91
92
|
JSON responses contain `trace_id`, process-scoped `session_id`, `device`,
|
|
92
|
-
`backend:"perfetto"
|
|
93
|
+
`backend:"perfetto"` (Android) or `"xctrace"` (iOS), `state` (starting/recording/stopping/completed/cancelled/failed),
|
|
93
94
|
nullable `reason` and `completion_reason`, host UTC capture bounds, nullable
|
|
94
|
-
`artifact`, `cleanup`, `correlation_ids
|
|
95
|
-
`unavailable_categories`. Capture bounds include command/acknowledgement latency.
|
|
95
|
+
`artifact`, `cleanup`, and `correlation_ids`. Android additionally returns
|
|
96
|
+
`enabled_sources` and `unavailable_categories`. Capture bounds include command/acknowledgement latency.
|
|
96
97
|
|
|
97
|
-
`artifact` contains absolute `path`, `format:"perfetto"` and `size_bytes
|
|
98
|
+
Android `artifact` contains absolute `path`, `format:"perfetto"` and `size_bytes`;
|
|
99
|
+
see the iOS section below for Instruments artifact and session fields.
|
|
98
100
|
`cleanup` contains `status` (pending/complete/failed) and `remaining_resources`.
|
|
99
101
|
Errors preserve known session metadata and set MCP `isError`. Argument/unknown-ID
|
|
100
102
|
errors return `{error:{tool,code,message}}` without a session. Codes include
|
|
@@ -105,7 +107,7 @@ Capability failures return ordinary `supported:false` responses with reasons.
|
|
|
105
107
|
Read state/cleanup independently: a successful artifact transfer can still have
|
|
106
108
|
failed device-file cleanup. Retry stop/status, or cancel to discard failed output.
|
|
107
109
|
|
|
108
|
-
##
|
|
110
|
+
## Android limits and privacy
|
|
109
111
|
|
|
110
112
|
The fixed preset requests sched, freq, idle, am, wm, gfx, view and input categories,
|
|
111
113
|
a 16 MiB ring buffer and a 32 MiB maximum output. Missing optional categories are
|
|
@@ -127,8 +129,73 @@ a uniquely named bounded device file for manual removal; restart performs no
|
|
|
127
129
|
broad cleanup sweep. Device suspend can extend Perfetto's own monotonic duration
|
|
128
130
|
limit; the server timer bounds wall-clock lifetime while the server is running.
|
|
129
131
|
|
|
132
|
+
## iOS simulator Instruments capture
|
|
133
|
+
|
|
134
|
+
Requires macOS, full Xcode with Instruments and clang, a booted simulator visible
|
|
135
|
+
in both `simctl` and `xctrace list devices`, and an already running development
|
|
136
|
+
app. Journey input also needs a compatible idb companion; capability discovery
|
|
137
|
+
for capture does not establish HID input support. The initial verified target is iPhone 17 Pro simulator / iOS 26.4.1 with
|
|
138
|
+
Xcode 27.2. Physical-device capture is unsupported and unrun. Tool installation,
|
|
139
|
+
OS profiling permissions and pairing/security changes are user actions.
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{"platform":"ios","deviceId":"<simulator-UDID>","appId":"<bundle-id>","template":"Time Profiler","durationMs":60000}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Use this target with capabilities and start, then bracket a `run_journey` on the
|
|
146
|
+
same simulator using the returned `systemTraceId`, and stop/cancel as above.
|
|
147
|
+
Omitting `template` defaults to Time Profiler. `appId` and `template` are rejected
|
|
148
|
+
on Android. Capabilities do not start capture; permissions and symbols remain
|
|
149
|
+
`unknown` until separately established. Installed templates are listed separately
|
|
150
|
+
from support: System Trace scheduling coverage is unverified, Animation Hitches
|
|
151
|
+
reported unsupported on the tested simulator, and Hangs is bundled within Time
|
|
152
|
+
Profiler rather than a selectable standalone template. No silent substitution.
|
|
153
|
+
|
|
154
|
+
The server resolves the bundle to an exact current executable/PID, pins the UDID,
|
|
155
|
+
and waits for a uniquely named Darwin recording-start notification before
|
|
156
|
+
returning `recording`. It compiles a small private listener with Xcode clang in
|
|
157
|
+
the owned temporary directory. A failed/uncertain start cleans up and returns a
|
|
158
|
+
failure, not an automatic retry. Errors such as `APP_NOT_RUNNING`,
|
|
159
|
+
`PROFILING_PERMISSION_REQUIRED`, `READINESS_TIMEOUT` and `TARGET_DISCONNECTED`
|
|
160
|
+
include bounded recovery guidance; raw recorder output is not returned.
|
|
161
|
+
|
|
162
|
+
Only the session-owned recorder is signalled; cleanup never terminates the app.
|
|
163
|
+
iOS readiness has a 35-second budget, stop/finalisation 30 seconds and artifact
|
|
164
|
+
validation 30 seconds. Recording is bounded by native duration and server
|
|
165
|
+
monitoring. Monitoring retains the pinned app identity and tolerates two consecutive
|
|
166
|
+
non-definitive command failures, resetting after a successful poll. The third
|
|
167
|
+
fails capture with its bounded reason (`COMMAND_FAILED`, `ACTION_TIMEOUT`, or
|
|
168
|
+
`TRACE_MONITOR_FAILED` for an unclassified failure). Storage limits, changed app
|
|
169
|
+
identity and a missing running app fail immediately. Expiry and recorder exit
|
|
170
|
+
are still checked after a tolerated poll failure. Shutdown attempts cancellation under an overall ten-second budget.
|
|
171
|
+
Failed export can be retried using stop/status without starting another recording;
|
|
172
|
+
cancel removes owned partial files. Completed artifacts remain for user deletion.
|
|
173
|
+
|
|
174
|
+
The `.trace` result may be a bundle/directory. `artifact` adds
|
|
175
|
+
`format:"instruments"`, `kind`, recursive `size_bytes`, and
|
|
176
|
+
`validation:{readable,schema,sample_rows}`. Validation checks a readable TOC,
|
|
177
|
+
matching target UUID/PID/template, `time-sample` schema and exported sample data.
|
|
178
|
+
Zero rows mean no CPU sample evidence. Warnings are preserved as bounded
|
|
179
|
+
classifications. This establishes readable capture, not successful symbolication,
|
|
180
|
+
working hang detection or root-cause diagnosis. Do not treat a saved failed trace
|
|
181
|
+
as a successful recording.
|
|
182
|
+
|
|
183
|
+
Results also include `app_id`, `pid`, `executable`, `template`,
|
|
184
|
+
`recording_ready_at_ms`, `warnings` and `clock_alignment:"unknown"`. The profiled
|
|
185
|
+
app stays pinned even if the journey navigates elsewhere; linked IDs do not prove
|
|
186
|
+
that other apps are captured. Start/stop during a linked journey reports partial
|
|
187
|
+
coverage. Host capture bounds include startup/finalisation uncertainty.
|
|
188
|
+
|
|
189
|
+
At least 1 GiB free output space is required before recording. The server checks
|
|
190
|
+
a 256 MiB recursive size threshold each second; this is a soft limit and can be
|
|
191
|
+
overshot during polling/finalisation. Oversized output fails and is discarded.
|
|
192
|
+
Final bundles reject symlinks and use restricted file/directory permissions.
|
|
193
|
+
Even app-attached capture may include sensitive system metadata, paths, symbols,
|
|
194
|
+
logs and signposts. Keep artifacts local and delete them when finished; no upload,
|
|
195
|
+
base64 response, permission bypass or restart-wide cleanup sweep occurs.
|
|
196
|
+
|
|
130
197
|
## Related guides
|
|
131
198
|
|
|
132
199
|
- [Interaction](interact.md#interaction-execution-controls-rfc-018): run the journey being measured.
|
|
133
200
|
- [Evidence correlation](correlated-evidence.md): interpret action and observation timing.
|
|
134
|
-
- [
|
|
201
|
+
- [Runtime debugger](debugger.md): inspect paused application state.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mobile-debug-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.0",
|
|
4
4
|
"description": "MCP server for mobile app debugging (Android + iOS), with focus on security and reliability",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"test": "npm run test:unit",
|
|
20
20
|
"lint": "eslint src test --quiet && npm run check:quality && npm run check:architecture",
|
|
21
21
|
"lint:fix": "eslint src test --fix && npm run check:quality && npm run check:architecture",
|
|
22
|
+
"package:antigravity": "node scripts/agent-plugin.mjs --export-antigravity",
|
|
22
23
|
"sync:agent-plugin": "node scripts/agent-plugin.mjs --sync",
|
|
23
24
|
"check:quality": "node scripts/code-quality.mjs",
|
|
24
25
|
"check:architecture": "node scripts/check-architecture.mjs",
|
package/scripts/agent-plugin.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
|
-
import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { copyFileSync, cpSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
3
3
|
import { fileURLToPath } from 'node:url'
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
|
|
@@ -43,22 +43,53 @@ const cursorManifest = {
|
|
|
43
43
|
},
|
|
44
44
|
},
|
|
45
45
|
}
|
|
46
|
+
const antigravityManifest = {
|
|
47
|
+
$schema: 'https://antigravity.google/schemas/v1/plugin.json',
|
|
48
|
+
...sharedMetadata,
|
|
49
|
+
displayName: extensions['com.openai'].interface.displayName,
|
|
50
|
+
logo: 'assets/mobile-debug-tools.svg',
|
|
51
|
+
suggestedPrompts: extensions['com.openai'].interface.defaultPrompt,
|
|
52
|
+
}
|
|
53
|
+
const antigravityMcp = {
|
|
54
|
+
mcpServers: Object.fromEntries(Object.entries(codexMcp.mcpServers).map(([name, server]) => {
|
|
55
|
+
const { type: _transport, ...settings } = server
|
|
56
|
+
return [name, settings]
|
|
57
|
+
})),
|
|
58
|
+
}
|
|
59
|
+
const mcpConfigs = [
|
|
60
|
+
['.mcp.json', codexMcp],
|
|
61
|
+
['mcp_config.json', antigravityMcp],
|
|
62
|
+
]
|
|
46
63
|
const manifests = [
|
|
47
64
|
['.codex-plugin/plugin.json', codexManifest],
|
|
48
65
|
['.claude-plugin/plugin.json', claudeManifest],
|
|
49
66
|
['.cursor-plugin/plugin.json', cursorManifest],
|
|
67
|
+
['.antigravity-plugin/plugin.json', antigravityManifest],
|
|
50
68
|
]
|
|
51
69
|
|
|
70
|
+
const cursorFiles = [
|
|
71
|
+
['.cursor-plugin/plugin.json', `${JSON.stringify(cursorManifest, null, 2)}\n`],
|
|
72
|
+
['skills/mobile-debug-mcp/SKILL.md', readFileSync(packagedSkill)],
|
|
73
|
+
['LICENSE', readFileSync(sourceLicense)],
|
|
74
|
+
['README.md', '# Mobile Debug Tools for Cursor\n\nGenerated by `npm run sync:agent-plugin`; edit the canonical plugin source.\n\nCopy the contents of this directory into `~/.cursor/plugins/local/mobile-debug-tools`,\nthen run Developer: Reload Window and check Customize for the skill and MCP server.\nDo not copy the portable parent plugin: its `${PLUGIN_DATA}` variable is not expanded by Cursor.\n\nSetup and support: https://github.com/clivejefferies/mobile-debug-tools\n'],
|
|
75
|
+
]
|
|
52
76
|
if (process.argv[2] === '--sync') {
|
|
53
77
|
copyFileSync(sourceLicense, packagedLicense)
|
|
54
|
-
|
|
78
|
+
for (const [relativePath, contents] of cursorFiles) {
|
|
79
|
+
const target = path.join(pluginRoot, 'cursor', relativePath)
|
|
80
|
+
mkdirSync(path.dirname(target), { recursive: true })
|
|
81
|
+
writeFileSync(target, contents)
|
|
82
|
+
}
|
|
83
|
+
for (const [relativePath, config] of mcpConfigs) {
|
|
84
|
+
writeFileSync(path.join(pluginRoot, relativePath), `${JSON.stringify(config, null, 2)}\n`)
|
|
85
|
+
}
|
|
55
86
|
for (const [relativePath, manifest] of manifests) {
|
|
56
87
|
const manifestPath = path.join(pluginRoot, relativePath)
|
|
57
88
|
mkdirSync(path.dirname(manifestPath), { recursive: true })
|
|
58
89
|
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
|
|
59
90
|
}
|
|
60
91
|
}
|
|
61
|
-
else if (process.argv[2]
|
|
92
|
+
else if (!['--check', '--export-antigravity'].includes(process.argv[2])) throw new Error('Use --sync, --check or --export-antigravity <new-directory>')
|
|
62
93
|
|
|
63
94
|
assert.equal(plugin.$schema, 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json')
|
|
64
95
|
assert.equal(plugin.name, 'mobile-debug-tools')
|
|
@@ -73,7 +104,10 @@ assert.equal(server.command, 'npx')
|
|
|
73
104
|
assert.deepEqual(server.args, ['--yes', `${packageInfo.name}@${packageInfo.version}`, 'server'])
|
|
74
105
|
assert.equal(server.cwd, '${PLUGIN_DATA}', 'npx must run outside the same-named source checkout')
|
|
75
106
|
assert.deepEqual(readFileSync(packagedLicense), readFileSync(sourceLicense), 'packaged license must match its source')
|
|
76
|
-
|
|
107
|
+
for (const [relativePath, config] of mcpConfigs) {
|
|
108
|
+
assert.deepEqual(JSON.parse(readFileSync(path.join(pluginRoot, relativePath), 'utf8')), config, `${relativePath} must match the portable MCP configuration`)
|
|
109
|
+
}
|
|
110
|
+
assert.ok(readFileSync(path.join(pluginRoot, antigravityManifest.logo)).length > 0, 'Antigravity logo must be bundled')
|
|
77
111
|
for (const [relativePath, expectedManifest] of manifests) {
|
|
78
112
|
assert.deepEqual(
|
|
79
113
|
JSON.parse(readFileSync(path.join(pluginRoot, relativePath), 'utf8')),
|
|
@@ -81,6 +115,9 @@ for (const [relativePath, expectedManifest] of manifests) {
|
|
|
81
115
|
`${relativePath} must match the portable plugin`,
|
|
82
116
|
)
|
|
83
117
|
}
|
|
118
|
+
for (const [relativePath, contents] of cursorFiles) {
|
|
119
|
+
assert.deepEqual(readFileSync(path.join(pluginRoot, 'cursor', relativePath)), Buffer.from(contents), `Cursor native ${relativePath} must match its source`)
|
|
120
|
+
}
|
|
84
121
|
for (const marketplacePath of [
|
|
85
122
|
'.agents/plugins/marketplace.json',
|
|
86
123
|
'.claude-plugin/marketplace.json',
|
|
@@ -90,9 +127,22 @@ for (const marketplacePath of [
|
|
|
90
127
|
assert.equal(marketplace.name, plugin.name, `${marketplacePath} name must match the plugin`)
|
|
91
128
|
assert.equal(marketplace.plugins.length, 1, `${marketplacePath} must expose one plugin`)
|
|
92
129
|
assert.equal(marketplace.plugins[0].name, plugin.name, `${marketplacePath} plugin name must match`)
|
|
93
|
-
assert.equal(marketplace.plugins[0].source?.path ?? marketplace.plugins[0].source, './agent-plugin', `${marketplacePath} source must point to the plugin`)
|
|
130
|
+
assert.equal(marketplace.plugins[0].source?.path ?? marketplace.plugins[0].source, marketplacePath.startsWith('.cursor-plugin') ? './agent-plugin/cursor' : './agent-plugin', `${marketplacePath} source must point to the plugin`)
|
|
94
131
|
}
|
|
95
132
|
const skillName = readFileSync(packagedSkill, 'utf8').match(/^---\r?\nname: ([a-z0-9-]+)\r?\n/)?.[1]
|
|
96
133
|
assert.equal(skillName, path.basename(path.dirname(packagedSkill)), 'packaged skill directory must match its frontmatter name')
|
|
97
134
|
|
|
98
135
|
console.log('Agent plugin package is in sync')
|
|
136
|
+
|
|
137
|
+
if (process.argv[2] === '--export-antigravity') {
|
|
138
|
+
assert.ok(process.argv[3], 'Supply a new output directory for the Antigravity export')
|
|
139
|
+
const target = path.resolve(process.argv[3])
|
|
140
|
+
// Refuse existing directories rather than overwrite source or stale package files.
|
|
141
|
+
mkdirSync(target)
|
|
142
|
+
writeFileSync(path.join(target, 'plugin.json'), `${JSON.stringify(antigravityManifest, null, 2)}\n`)
|
|
143
|
+
writeFileSync(path.join(target, 'mcp_config.json'), `${JSON.stringify(antigravityMcp, null, 2)}\n`)
|
|
144
|
+
copyFileSync(path.join(pluginRoot, '.antigravity-plugin/README.md'), path.join(target, 'README.md'))
|
|
145
|
+
copyFileSync(packagedLicense, path.join(target, 'LICENSE'))
|
|
146
|
+
for (const directory of ['skills', 'assets']) cpSync(path.join(pluginRoot, directory), path.join(target, directory), { recursive: true, filter: file => path.basename(file) !== '.DS_Store' && !path.basename(file).startsWith('._') })
|
|
147
|
+
console.log(`Antigravity package exported to ${target}`)
|
|
148
|
+
}
|
package/src/debugger/android.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { execFile } from 'node:child_process'
|
|
|
3
3
|
import { promisify } from 'node:util'
|
|
4
4
|
import { DebuggerError, debuggerRemaining, debuggerSignal, nativeError } from './worker.js'
|
|
5
5
|
const exec = promisify(execFile)
|
|
6
|
-
export interface DebugTarget { app_id: string; pid: number; process_name: string; start_identity: string; uid: number }
|
|
6
|
+
export interface DebugTarget { app_id: string; pid: number; process_name: string; start_identity: string; uid: number | null; executable_path?: string }
|
|
7
7
|
export interface DebugNative {
|
|
8
8
|
candidates(device: string, app: string): Promise<DebugTarget[]>
|
|
9
9
|
identity(device: string, target: DebugTarget): Promise<DebugTarget>
|
package/src/debugger/guard.ts
CHANGED
|
@@ -5,7 +5,7 @@ interface PauseEvidence {
|
|
|
5
5
|
captured_at_ms: number
|
|
6
6
|
target: unknown
|
|
7
7
|
}
|
|
8
|
-
let pauseProvider: (device: string) => PauseEvidence | null = () => null
|
|
8
|
+
let pauseProvider: (device: string, platform?: string) => PauseEvidence | null = () => null
|
|
9
9
|
export function registerDebuggerPauseProvider(provider: typeof pauseProvider) { pauseProvider = provider }
|
|
10
10
|
const context = new AsyncLocalStorage<{ platform: string; device: string; dispatched: boolean }>()
|
|
11
11
|
export function withDebuggerGuard<T>(platform: string, device: string, operation: () => Promise<T>) {
|
|
@@ -14,13 +14,13 @@ export function withDebuggerGuard<T>(platform: string, device: string, operation
|
|
|
14
14
|
// Synchronous checks and native submission share a JS turn with pause updates.
|
|
15
15
|
export function checkDebuggerGuard(dispatch = false) {
|
|
16
16
|
const current = context.getStore()
|
|
17
|
-
if (!current
|
|
18
|
-
if (pauseProvider(current.device)) throw new DebuggerError('DEBUGGER_PAUSED')
|
|
17
|
+
if (!current) return
|
|
18
|
+
if (pauseProvider(current.device, current.platform)) throw new DebuggerError('DEBUGGER_PAUSED')
|
|
19
19
|
if (dispatch) current.dispatched = true
|
|
20
20
|
}
|
|
21
21
|
export function guardDispatched() { return context.getStore()?.dispatched ?? false }
|
|
22
|
-
export function pausedReadiness(device: string) {
|
|
23
|
-
const paused = pauseProvider(device)
|
|
22
|
+
export function pausedReadiness(device: string, platform = 'android') {
|
|
23
|
+
const paused = pauseProvider(device, platform)
|
|
24
24
|
if (!paused) return null
|
|
25
25
|
return { state: 'debugger_paused' as const, scope: 'device_ui' as const, basis: 'observed' as const,
|
|
26
26
|
captured_at_ms: paused.captured_at_ms, sample_count: 0, stable_for_ms: 0, snapshot_session_id: null, snapshot_revision: null,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url'
|
|
2
|
+
import { getXcrunCmd } from '../utils/ios/utils.js'
|
|
3
|
+
import { iosExec } from './ios.js'
|
|
4
|
+
import { DebuggerProcess, type WorkerEvent, type DebugWorker } from './worker.js'
|
|
5
|
+
import { DebuggerError } from './diagnostics.js'
|
|
6
|
+
export const iosCapabilities = { attach: true, breakpoints: true, stacks: true, locals: true, step: true, resume: true,
|
|
7
|
+
evaluation: false, mutation: false, coroutines: false, native: false, async_execution_inspection: true, async_stepping: true }
|
|
8
|
+
export const iosLimitations = ['IOS_SIMULATOR_ONLY', 'DEBUG_METADATA_REQUIRED', 'SHALLOW_LOCALS', 'ALL_THREADS_SUSPEND', 'DEVICE_WIDE_UI_GUARD', 'ASYNC_STEPPING_TOOLCHAIN_DEPENDENT', 'AGGREGATE_VALUES_TYPE_ONLY', 'NO_TASK_ENUMERATION']
|
|
9
|
+
export const iosWorkerSource = fileURLToPath(new URL('../../src/debugger/lldb/worker.py', import.meta.url))
|
|
10
|
+
let prepared: Promise<{ python: string; env: NodeJS.ProcessEnv }> | undefined
|
|
11
|
+
export function prepareIOSWorker() {
|
|
12
|
+
prepared ??= (async () => {
|
|
13
|
+
if (process.platform !== 'darwin') throw new DebuggerError('DEBUGGER_UNSUPPORTED')
|
|
14
|
+
const pythonPath = await iosExec(getXcrunCmd(), ['lldb', '-P'])
|
|
15
|
+
const python = await iosExec(getXcrunCmd(), ['--find', 'python3'])
|
|
16
|
+
// Ignore user Python startup/customisation and LLDB init files.
|
|
17
|
+
const env = { PATH: process.env.PATH, HOME: process.env.HOME, PYTHONPATH: pythonPath, PYTHONNOUSERSITE: '1' }
|
|
18
|
+
const ready = await iosExec(python, ['-s', '-B', '-c', 'import lldb; d=lldb.SBDebugger.Create(False); print("ready" if d.IsValid() else "unavailable"); lldb.SBDebugger.Destroy(d)'], env)
|
|
19
|
+
if (ready !== 'ready') throw new DebuggerError('DEBUGGER_UNSUPPORTED')
|
|
20
|
+
return { python, env }
|
|
21
|
+
})().catch(error => { prepared = undefined; throw error })
|
|
22
|
+
return prepared
|
|
23
|
+
}
|
|
24
|
+
export async function createIOSWorker(onEvent: (event: WorkerEvent) => void) {
|
|
25
|
+
const { python, env } = await prepareIOSWorker()
|
|
26
|
+
let released = false
|
|
27
|
+
const worker = new DebuggerProcess(python, '', event => {
|
|
28
|
+
if (event.event === 'terminated' && ['process_exit', 'detached', 'pause_limit', 'idle_limit', 'session_limit'].includes(String(event.reason))) released = true
|
|
29
|
+
onEvent(event)
|
|
30
|
+
}, { args: ['-u', '-s', '-B', iosWorkerSource], env })
|
|
31
|
+
return new IOSWorker(worker, () => released)
|
|
32
|
+
}
|
|
33
|
+
/** A dead worker is not proof that its owned suspension was released. */
|
|
34
|
+
export class IOSWorker implements DebugWorker {
|
|
35
|
+
private detached = false
|
|
36
|
+
constructor(private worker: DebugWorker, private released: () => boolean) {}
|
|
37
|
+
async call(command: string, args: Record<string, unknown> = {}, timeout = 10000) {
|
|
38
|
+
const result = await this.worker.call(command, args, timeout)
|
|
39
|
+
if (command === 'detach' && result.detached === true) this.detached = true
|
|
40
|
+
return result
|
|
41
|
+
}
|
|
42
|
+
async close() {
|
|
43
|
+
if (!this.detached && !this.released()) {
|
|
44
|
+
try { await this.call('detach', {}, 1000) } catch { /* Report uncertainty after bounded transport shutdown. */ }
|
|
45
|
+
}
|
|
46
|
+
await this.worker.close()
|
|
47
|
+
if (!this.detached && !this.released()) throw new DebuggerError('DEBUGGER_CLEANUP_FAILED')
|
|
48
|
+
}
|
|
49
|
+
}
|