mobile-debug-mcp 0.35.0 → 0.37.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 +96 -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 +261 -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 +46 -18
- package/dist/contracts/device-readiness.js +1 -0
- 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/interact/waits/ui-change-wait.js +9 -2
- package/dist/interact/waits/ui-wait.js +7 -3
- package/dist/manage/index.js +3 -0
- package/dist/manage/ios.js +2 -0
- package/dist/observe/device-readiness.js +116 -0
- package/dist/observe/evidence.js +31 -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 +246 -0
- package/dist/observe/readiness-service.js +22 -10
- package/dist/observe/readiness.js +34 -4
- package/dist/observe/screenshot-encoding.js +4 -0
- package/dist/observe/snapshot-collection.js +7 -2
- 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/definitions/manage.js +3 -2
- package/dist/server/handlers/action-execution.js +23 -14
- package/dist/server/handlers/compact-interaction.js +106 -0
- package/dist/server/handlers/compact-state.js +47 -0
- package/dist/server/handlers/debugger-handlers.js +27 -8
- package/dist/server/handlers/diagnostic.js +12 -4
- package/dist/server/handlers/input.js +2 -0
- package/dist/server/handlers/interact.js +2 -0
- package/dist/server/handlers/interaction-diagnostics.js +62 -0
- package/dist/server/handlers/journey.js +18 -20
- package/dist/server/handlers/observe.js +2 -1
- package/dist/server/handlers/tool-handlers.js +28 -14
- package/dist/server/schemas/debugger-schema.js +10 -7
- package/dist/server/schemas/interaction-diagnostics.js +9 -0
- package/dist/server/schemas/system-trace-schema.js +14 -9
- package/dist/server/tool-definitions.js +9 -4
- package/dist/server-core.js +5 -2
- package/dist/server.js +5 -5
- package/dist/system/android.js +19 -15
- package/dist/system/index.js +2 -1
- package/dist/utils/android/utils.js +7 -16
- package/dist/utils/app-installation.js +77 -0
- package/dist/utils/benchmark-metrics.js +43 -0
- package/dist/utils/exec.js +4 -0
- package/dist/utils/ios/utils.js +7 -11
- package/dist/utils/resolve-device.js +3 -2
- package/dist/utils/timing.js +3 -3
- package/docs/CHANGELOG.md +48 -1
- package/docs/agent-plugin-installation-verification.md +39 -0
- package/docs/benchmarks/README.md +1 -0
- package/docs/benchmarks/end-to-end-latency/README.md +98 -0
- package/docs/benchmarks/end-to-end-latency/android-snapshot-pair.json +6532 -0
- package/docs/benchmarks/end-to-end-latency/android-snapshot-pair.md +30 -0
- package/docs/benchmarks/end-to-end-latency/baseline-attempt-1.json +15901 -0
- package/docs/benchmarks/end-to-end-latency/baseline-attempt-1.md +14 -0
- package/docs/benchmarks/end-to-end-latency/baseline.json +233843 -0
- package/docs/benchmarks/end-to-end-latency/baseline.md +21 -0
- package/docs/benchmarks/end-to-end-latency/investigation.md +128 -0
- package/docs/benchmarks/end-to-end-latency/ios-smoke.json +84 -0
- package/docs/benchmarks/end-to-end-latency/ios-smoke.md +9 -0
- package/docs/benchmarks/end-to-end-latency/smoke.json +19601 -0
- package/docs/benchmarks/end-to-end-latency/smoke.md +21 -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 +166 -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/specs/033-device-readiness-blockers/android-evidence.json +437 -0
- package/docs/specs/033-device-readiness-blockers/android-physical-evidence.json +28 -0
- package/docs/specs/033-device-readiness-blockers/android-physical-power-evidence.json +29 -0
- package/docs/specs/033-device-readiness-blockers/intent.md +41 -0
- package/docs/specs/033-device-readiness-blockers/ios-investigation.md +55 -0
- package/docs/specs/033-device-readiness-blockers/ios-probe-evidence.json +24 -0
- package/docs/specs/033-device-readiness-blockers/ios-tool-evidence.json +73 -0
- package/docs/specs/033-device-readiness-blockers/plan.md +249 -0
- package/docs/specs/033-device-readiness-blockers/spec.md +134 -0
- package/docs/specs/034-app-installation-status/intent.md +41 -0
- package/docs/specs/034-app-installation-status/native-evidence.json +654 -0
- package/docs/specs/034-app-installation-status/plan.md +73 -0
- package/docs/specs/034-app-installation-status/spec.md +114 -0
- package/docs/specs/035-compact-interaction-results/baseline.md +45 -0
- package/docs/specs/035-compact-interaction-results/intent.md +38 -0
- package/docs/specs/035-compact-interaction-results/plan.md +43 -0
- package/docs/specs/035-compact-interaction-results/spec.md +118 -0
- package/docs/specs/035-compact-interaction-results/verification.md +111 -0
- package/docs/specs/036-end-to-end-interaction-latency/android-snapshot-amendment.md +60 -0
- package/docs/specs/036-end-to-end-interaction-latency/android-snapshot-plan.md +32 -0
- package/docs/specs/036-end-to-end-interaction-latency/intent.md +37 -0
- package/docs/specs/036-end-to-end-interaction-latency/plan.md +59 -0
- package/docs/specs/036-end-to-end-interaction-latency/spec.md +126 -0
- package/docs/specs/036-end-to-end-interaction-latency/verification.md +87 -0
- package/docs/tools/TOOLS.md +3 -2
- package/docs/tools/compact-interactions.md +56 -0
- package/docs/tools/correlated-evidence.md +6 -0
- package/docs/tools/debugger.md +45 -5
- package/docs/tools/interact.md +17 -8
- package/docs/tools/manage.md +10 -2
- package/docs/tools/observe.md +42 -2
- package/docs/tools/system-traces.md +81 -14
- package/docs/tools/system.md +6 -0
- package/package.json +2 -1
- package/scripts/agent-plugin.mjs +55 -5
- package/src/contracts/device-readiness.ts +8 -0
- 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/interact/waits/ui-change-wait.ts +7 -2
- package/src/interact/waits/ui-wait.ts +6 -3
- package/src/manage/index.ts +2 -0
- package/src/manage/ios.ts +2 -0
- package/src/observe/device-readiness.ts +96 -0
- package/src/observe/evidence.ts +30 -1
- 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 +206 -0
- package/src/observe/readiness-service.ts +22 -10
- package/src/observe/readiness.ts +37 -4
- package/src/observe/screenshot-encoding.ts +4 -0
- package/src/observe/snapshot-collection.ts +7 -2
- 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/common.ts +2 -2
- package/src/server/definitions/manage.ts +3 -2
- package/src/server/handlers/action-execution.ts +22 -14
- package/src/server/handlers/compact-interaction.ts +91 -0
- package/src/server/handlers/compact-state.ts +43 -0
- package/src/server/handlers/debugger-handlers.ts +28 -9
- package/src/server/handlers/diagnostic.ts +11 -4
- package/src/server/handlers/input.ts +2 -0
- package/src/server/handlers/interact.ts +2 -0
- package/src/server/handlers/interaction-diagnostics.ts +50 -0
- package/src/server/handlers/journey.ts +15 -17
- package/src/server/handlers/observe.ts +2 -1
- package/src/server/handlers/payload.ts +2 -1
- package/src/server/handlers/tool-handlers.ts +29 -14
- package/src/server/schemas/debugger-schema.ts +10 -7
- package/src/server/schemas/interaction-diagnostics.ts +10 -0
- package/src/server/schemas/system-trace-schema.ts +15 -9
- package/src/server/tool-definitions.ts +8 -4
- package/src/server-core.ts +5 -2
- package/src/server.ts +4 -4
- package/src/system/android.ts +18 -11
- package/src/system/index.ts +2 -1
- package/src/types.ts +2 -0
- package/src/utils/android/utils.ts +8 -10
- package/src/utils/app-installation.ts +73 -0
- package/src/utils/benchmark-metrics.ts +42 -0
- package/src/utils/exec.ts +5 -0
- package/src/utils/ios/utils.ts +7 -10
- package/src/utils/resolve-device.ts +4 -2
- package/src/utils/timing.ts +3 -2
- package/test/device/automated/debugger/runtime.android.test.ts +5 -0
- package/test/device/automated/debugger/runtime.ios.test.ts +227 -0
- package/test/device/automated/interact/compact-results.smoke.ts +58 -0
- package/test/device/automated/latency/end-to-end-environment.ts +61 -0
- package/test/device/automated/latency/end-to-end-report.ts +90 -0
- package/test/device/automated/latency/end-to-end-session.ts +98 -0
- package/test/device/automated/latency/end-to-end.ts +149 -0
- package/test/device/automated/latency/owned-transport.ts +81 -0
- package/test/device/automated/latency/snapshot-pair.ts +143 -0
- package/test/device/automated/latency-ios/benchmark.ts +126 -0
- package/test/device/automated/observe/correlated-evidence.smoke.ts +7 -5
- package/test/device/automated/observe/device-readiness-cleanup.ts +14 -0
- package/test/device/automated/observe/device-readiness.android.smoke.ts +78 -0
- package/test/device/automated/observe/ios-system-trace.smoke.ts +119 -0
- package/test/device/automated/observe/system-trace.smoke.ts +2 -2
- package/test/fixtures/contracts/tool-definitions.json +243 -47
- 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/fixtures/latency-journey-app/build.ts +3 -3
- package/test/helpers/ios-trace-shutdown.ts +29 -0
- package/test/unit/debugger/handle-guards.test.ts +89 -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/interact/ui-change-readiness-interruption.test.ts +37 -0
- package/test/unit/observe/capture_debug_snapshot.test.ts +66 -12
- package/test/unit/observe/device-readiness-cleanup.test.ts +24 -0
- package/test/unit/observe/device-readiness.test.ts +97 -0
- package/test/unit/observe/ios-trace-native.test.ts +90 -0
- package/test/unit/observe/ios-trace-sessions.test.ts +154 -0
- package/test/unit/observe/readiness-retained-blockers.test.ts +40 -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/compact-interaction.test.ts +87 -0
- package/test/unit/server/compact-journey-evidence.test.ts +40 -0
- package/test/unit/server/compact-review-regressions.test.ts +78 -0
- package/test/unit/server/conditional_actions.test.ts +7 -3
- package/test/unit/server/correlated-evidence.test.ts +18 -6
- package/test/unit/server/end-to-end-report.test.ts +46 -0
- package/test/unit/server/input-device-readiness.test.ts +55 -0
- package/test/unit/server/interaction-diagnostics.test.ts +45 -0
- package/test/unit/server/owned-benchmark-transport.test.ts +32 -0
- package/test/unit/server/readiness-retained-deadline.test.ts +32 -0
- package/test/unit/server/readiness.test.ts +24 -1
- package/test/unit/server/run_journey.test.ts +2 -1
- package/test/unit/server/selector_tap_target.test.ts +2 -1
- package/test/unit/server/system-trace.test.ts +18 -7
- package/test/unit/utils/app-installation.test.ts +78 -0
- package/test/unit/utils/benchmark-metrics.test.ts +36 -0
- package/test/unit/utils/resolve_device.test.ts +11 -0
|
@@ -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/docs/tools/system.md
CHANGED
|
@@ -10,6 +10,12 @@ required platform is available, continue with [device selection](manage.md#list_
|
|
|
10
10
|
## get_system_status
|
|
11
11
|
A fast, non-throwing healthcheck that inspects key dependencies and connections required for mobile debugging.
|
|
12
12
|
|
|
13
|
+
Android installation checks use `MCP_TARGET_PACKAGE` then `MCP_TARGET_APP_ID`.
|
|
14
|
+
`appInstallations` contains device-specific installed/absent/unknown observations
|
|
15
|
+
with app ID, device ID, scope and timestamp. Legacy `appInstalled` is omitted
|
|
16
|
+
unless exactly one device has a conclusive result. Missing context and failed
|
|
17
|
+
queries never establish absence; these fields do not report iOS installation.
|
|
18
|
+
|
|
13
19
|
Input:
|
|
14
20
|
|
|
15
21
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mobile-debug-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.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
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface DeviceReadiness {
|
|
2
|
+
wakefulness: 'awake' | 'asleep' | 'transitioning' | 'unknown' | 'unsupported'
|
|
3
|
+
lock_state: 'locked' | 'unlocked' | 'unknown' | 'unsupported'
|
|
4
|
+
input_ready: false | null
|
|
5
|
+
blockers: Array<'DEVICE_ASLEEP' | 'DEVICE_LOCKED' | 'DEVICE_TRANSITIONING' | 'DEBUGGER_PAUSED'>
|
|
6
|
+
evidence: Array<{ code: string; source: string; captured_at_ms: number }>
|
|
7
|
+
limitations: string[]
|
|
8
|
+
}
|
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
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process'
|
|
2
|
+
import { promisify } from 'node:util'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { realpath } from 'node:fs/promises'
|
|
5
|
+
import { getXcrunCmd } from '../utils/ios/utils.js'
|
|
6
|
+
import type { DebugNative, DebugTarget } from './android.js'
|
|
7
|
+
import { DebuggerError, debuggerRemaining, debuggerSignal, nativeError } from './worker.js'
|
|
8
|
+
const exec = promisify(execFile)
|
|
9
|
+
export async function iosExec(file: string, args: string[], env?: NodeJS.ProcessEnv) {
|
|
10
|
+
try { return (await exec(file, args, { timeout: debuggerRemaining(5000), signal: debuggerSignal(), maxBuffer: 1024 * 1024, ...(env ? { env } : {}) })).stdout.trim() }
|
|
11
|
+
catch (e) { throw nativeError(e, 'DEVICE_UNAVAILABLE') }
|
|
12
|
+
}
|
|
13
|
+
export class IOSDebugNative implements DebugNative {
|
|
14
|
+
constructor(private run = iosExec) {}
|
|
15
|
+
private sim(args: string[]) { return this.run(getXcrunCmd(), ['simctl', ...args]) }
|
|
16
|
+
async candidates(device: string, app: string): Promise<DebugTarget[]> {
|
|
17
|
+
if (!/^[A-Fa-f0-9]{8}(?:-[A-Fa-f0-9]{4}){3}-[A-Fa-f0-9]{12}$/.test(device) || !/^[A-Za-z0-9][A-Za-z0-9.-]+$/.test(app)) throw new DebuggerError('INVALID_ARGUMENT')
|
|
18
|
+
const inventory = JSON.parse(await this.sim(['list', 'devices', 'booted', '--json'])) as { devices: Record<string, { udid: string; state: string }[]> }
|
|
19
|
+
if (!Object.values(inventory.devices).flat().some(d => d.udid === device && d.state === 'Booted')) throw new DebuggerError('DEBUGGER_UNSUPPORTED')
|
|
20
|
+
const services = await this.sim(['spawn', device, 'launchctl', 'list'])
|
|
21
|
+
const pids = this.processes(services, app)
|
|
22
|
+
if (!pids.length) return []
|
|
23
|
+
const container = await this.sim(['get_app_container', device, app, 'app'])
|
|
24
|
+
const executable = await this.run('/usr/bin/plutil', ['-extract', 'CFBundleExecutable', 'raw', '-o', '-', path.join(container, 'Info.plist')])
|
|
25
|
+
if (path.basename(executable) !== executable || !executable) throw new DebuggerError('PROCESS_NOT_FOUND')
|
|
26
|
+
const executablePath = await realpath(path.join(container, executable))
|
|
27
|
+
return Promise.all(pids.map(pid => this.identity(device, { app_id: app, pid, process_name: executable, executable_path: executablePath, start_identity: '', uid: null })))
|
|
28
|
+
}
|
|
29
|
+
private processes(services: string, app: string) {
|
|
30
|
+
return services.split('\n').flatMap(line => {
|
|
31
|
+
const [pid, , label] = line.trim().split(/\s+/)
|
|
32
|
+
return /^\d+$/.test(pid) && label?.startsWith(`UIKitApplication:${app}[`) ? [Number(pid)] : []
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
async identity(device: string, target: DebugTarget): Promise<DebugTarget> {
|
|
36
|
+
const services = await this.sim(['spawn', device, 'launchctl', 'list'])
|
|
37
|
+
if (!this.processes(services, target.app_id).includes(target.pid)) throw new DebuggerError('PROCESS_CHANGED')
|
|
38
|
+
const command = await this.run('/bin/ps', ['-ww', '-p', String(target.pid), '-o', 'comm='])
|
|
39
|
+
const started = await this.run('/bin/ps', ['-p', String(target.pid), '-o', 'lstart='])
|
|
40
|
+
if (command !== target.executable_path || !started || (target.start_identity && started !== target.start_identity)) throw new DebuggerError('PROCESS_CHANGED')
|
|
41
|
+
return { ...target, start_identity: started }
|
|
42
|
+
}
|
|
43
|
+
async forward(): Promise<number> { throw new DebuggerError('DEBUGGER_UNSUPPORTED') }
|
|
44
|
+
async removeForward() { throw new DebuggerError('DEBUGGER_UNSUPPORTED') }
|
|
45
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Owned source breakpoints with exact-line checking, including late image loads."""
|
|
2
|
+
import os
|
|
3
|
+
import uuid
|
|
4
|
+
import lldb
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Breakpoints:
|
|
8
|
+
def __init__(self, target):
|
|
9
|
+
self.target = target
|
|
10
|
+
self.owned = {}
|
|
11
|
+
|
|
12
|
+
def describe(self, key):
|
|
13
|
+
bp, source, line = self.owned[key]
|
|
14
|
+
locations = []
|
|
15
|
+
rejected = False
|
|
16
|
+
for loc in bp:
|
|
17
|
+
entry = loc.GetAddress().GetLineEntry()
|
|
18
|
+
actual = str(entry.GetFileSpec())
|
|
19
|
+
exact = entry.IsValid() and os.path.normpath(actual) == source and entry.GetLine() == line
|
|
20
|
+
loc.SetEnabled(exact)
|
|
21
|
+
if exact:
|
|
22
|
+
locations.append({"source_file": actual, "line": line})
|
|
23
|
+
else:
|
|
24
|
+
rejected = True
|
|
25
|
+
known_source = self.target.FindCompileUnits(lldb.SBFileSpec(source)).GetSize() > 0
|
|
26
|
+
status = "resolved" if locations else "rejected" if rejected or known_source else "pending"
|
|
27
|
+
return {"breakpoint_id": key, "status": status, "locations": locations}
|
|
28
|
+
|
|
29
|
+
def set(self, args):
|
|
30
|
+
source = os.path.normpath(args["sourceFile"])
|
|
31
|
+
bp = self.target.BreakpointCreateByLocation(lldb.SBFileSpec(source), args["line"], 0, 0, lldb.SBFileSpecList(), False)
|
|
32
|
+
key = str(uuid.uuid4())
|
|
33
|
+
self.owned[key] = (bp, source, args["line"])
|
|
34
|
+
result = self.describe(key)
|
|
35
|
+
if result["status"] == "rejected":
|
|
36
|
+
self.target.BreakpointDelete(bp.GetID())
|
|
37
|
+
del self.owned[key]
|
|
38
|
+
return result
|
|
39
|
+
|
|
40
|
+
def refresh(self):
|
|
41
|
+
return [self.describe(key) for key in list(self.owned)]
|
|
42
|
+
|
|
43
|
+
def remove(self, key):
|
|
44
|
+
if key in self.owned:
|
|
45
|
+
bp, _, _ = self.owned.pop(key)
|
|
46
|
+
self.target.BreakpointDelete(bp.GetID())
|
|
47
|
+
return {"removed": True}
|
|
48
|
+
|
|
49
|
+
def clear(self):
|
|
50
|
+
for key in list(self.owned):
|
|
51
|
+
self.remove(key)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Bounded, static inspection. Never evaluate expressions or object descriptions."""
|
|
2
|
+
import uuid
|
|
3
|
+
import re
|
|
4
|
+
import lldb
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def location(frame):
|
|
8
|
+
entry = frame.GetLineEntry()
|
|
9
|
+
file = entry.GetFileSpec()
|
|
10
|
+
source = str(file)[:512] if file.IsValid() else None
|
|
11
|
+
return {"source_file": source, "line": entry.GetLine() if entry.IsValid() else None,
|
|
12
|
+
"method_name": (frame.GetFunctionName() or "")[:512], "class_name": None}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Inspection:
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self.invalidate()
|
|
18
|
+
|
|
19
|
+
def invalidate(self):
|
|
20
|
+
self.stop_id = None
|
|
21
|
+
self.threads = {}
|
|
22
|
+
self.frames = {}
|
|
23
|
+
|
|
24
|
+
def stopped(self, process):
|
|
25
|
+
self.invalidate()
|
|
26
|
+
self.stop_id = str(uuid.uuid4())
|
|
27
|
+
selected = None
|
|
28
|
+
chosen = process.GetSelectedThread()
|
|
29
|
+
threads = [chosen] if chosen.IsValid() else []
|
|
30
|
+
for index in range(min(process.GetNumThreads(), 100)):
|
|
31
|
+
thread = process.GetThreadAtIndex(index)
|
|
32
|
+
if not chosen.IsValid() or thread.GetThreadID() != chosen.GetThreadID():
|
|
33
|
+
threads.append(thread)
|
|
34
|
+
for thread in threads[:100]:
|
|
35
|
+
key = str(uuid.uuid4())
|
|
36
|
+
self.threads[key] = thread
|
|
37
|
+
if selected is None and thread.GetStopReason() not in (lldb.eStopReasonNone, lldb.eStopReasonInvalid):
|
|
38
|
+
selected = key
|
|
39
|
+
if selected is None or process.GetState() != lldb.eStateStopped:
|
|
40
|
+
self.invalidate()
|
|
41
|
+
return None
|
|
42
|
+
thread = self.threads.get(selected)
|
|
43
|
+
return {"event": "stopped", "stop_id": self.stop_id, "thread_id": selected,
|
|
44
|
+
"reason": stop_reason(thread),
|
|
45
|
+
"location": location(thread.GetFrameAtIndex(0)) if thread else None,
|
|
46
|
+
"threads": [{"thread_id": key, "name": (t.GetName() or "")[:256]} for key, t in self.threads.items()],
|
|
47
|
+
"threads_truncated": process.GetNumThreads() > 100}
|
|
48
|
+
|
|
49
|
+
def thread(self, args):
|
|
50
|
+
if args.get("stopId") != self.stop_id or args.get("threadId") not in self.threads:
|
|
51
|
+
raise ValueError("DEBUGGER_STALE_REFERENCE")
|
|
52
|
+
return self.threads[args["threadId"]]
|
|
53
|
+
|
|
54
|
+
def stack(self, args):
|
|
55
|
+
thread = self.thread(args)
|
|
56
|
+
offset, limit = args.get("offset", 0), args.get("limit", 20)
|
|
57
|
+
count = min(thread.GetNumFrames(), 1000)
|
|
58
|
+
frames = []
|
|
59
|
+
for index in range(offset, min(count, offset + limit)):
|
|
60
|
+
frame = thread.GetFrameAtIndex(index)
|
|
61
|
+
key = self.stop_id + ":" + args["threadId"] + ":" + str(index)
|
|
62
|
+
if key not in self.frames and len(self.frames) >= 1000:
|
|
63
|
+
break
|
|
64
|
+
self.frames[key] = frame
|
|
65
|
+
frames.append({"frame_id": key, "index": index, **location(frame)})
|
|
66
|
+
return {"frames": frames, "truncated": count > offset + len(frames) or thread.GetNumFrames() > 1000}
|
|
67
|
+
|
|
68
|
+
def locals(self, args):
|
|
69
|
+
if args.get("stopId") != self.stop_id or args.get("frameId") not in self.frames:
|
|
70
|
+
raise ValueError("DEBUGGER_STALE_REFERENCE")
|
|
71
|
+
frame = self.frames[args["frameId"]]
|
|
72
|
+
values = frame.GetVariables(True, True, False, True, lldb.eNoDynamicValues)
|
|
73
|
+
result = []
|
|
74
|
+
for index in range(min(values.GetSize(), 100)):
|
|
75
|
+
value = values.GetValueAtIndex(index).GetNonSyntheticValue()
|
|
76
|
+
name, kind = (value.GetName() or "")[:256], (value.GetTypeName() or "")[:256]
|
|
77
|
+
if not value.GetError().Success():
|
|
78
|
+
result.append({"name": name, "type": kind, "availability": "unavailable"})
|
|
79
|
+
continue
|
|
80
|
+
primitive = value.GetType().GetTypeFlags() & (lldb.eTypeIsScalar | lldb.eTypeIsInteger | lldb.eTypeIsFloat)
|
|
81
|
+
text = value.GetValue() if primitive else None
|
|
82
|
+
# Swift value wrappers are not C scalars. Decode only standard primitive storage.
|
|
83
|
+
if kind in ("Swift.Int", "Swift.Int32", "Swift.Int64", "Swift.UInt", "Swift.Bool", "Swift.Double", "Swift.Float"):
|
|
84
|
+
storage = value.GetChildMemberWithName("_value").GetNonSyntheticValue()
|
|
85
|
+
if storage.IsValid() and storage.GetError().Success():
|
|
86
|
+
text = storage.GetValue()
|
|
87
|
+
if kind == "Swift.Bool" and text in ("0", "1"):
|
|
88
|
+
text = "true" if text == "1" else "false"
|
|
89
|
+
result.append({"name": name, "type": kind, "value": text[:256] if text else None,
|
|
90
|
+
"kind": "primitive" if text is not None else "object",
|
|
91
|
+
"truncated": bool(text and len(text) > 256),
|
|
92
|
+
"object_id": self.object_identity(value, kind) if text is None else None})
|
|
93
|
+
return {"availability": "available" if result else "unknown", "locals": result,
|
|
94
|
+
"reason": None if result else "EMPTY_OR_UNAVAILABLE_VARIABLE_METADATA",
|
|
95
|
+
"truncated": values.GetSize() > 100}
|
|
96
|
+
|
|
97
|
+
def object_identity(self, value, kind):
|
|
98
|
+
pointer = value.GetValue()
|
|
99
|
+
if pointer and re.fullmatch(r"0x[0-9a-fA-F]+", pointer):
|
|
100
|
+
return str(uuid.uuid5(uuid.UUID(self.stop_id), kind + ":" + pointer))
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def stop_reason(thread):
|
|
105
|
+
if thread is None:
|
|
106
|
+
return "unknown"
|
|
107
|
+
return {lldb.eStopReasonBreakpoint: "breakpoint", lldb.eStopReasonPlanComplete: "step",
|
|
108
|
+
lldb.eStopReasonSignal: "signal", lldb.eStopReasonException: "exception",
|
|
109
|
+
lldb.eStopReasonWatchpoint: "watchpoint", lldb.eStopReasonExec: "exec"}.get(thread.GetStopReason(), "unknown")
|