mobile-debug-mcp 0.34.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/.agents/skills/mcp-builder/SKILL.md +44 -64
- package/.agents/skills/mcp-builder/references/build-flags.md +44 -43
- package/.agents/skills/mcp-builder/references/diagnostics-schema.md +4 -1
- package/.agents/skills/mcp-builder/references/toolchain-details.md +49 -62
- package/.agents/skills/spec-review/references/spec-review-checklist.md +7 -0
- package/.agents/skills/test-authoring/SKILL.md +7 -6
- package/.agents/skills/test-authoring/references/repo-test-layout.md +3 -1
- package/.agents/skills/test-authoring/references/test-authoring-workflow.md +16 -7
- package/.agents/skills/test-authoring/references/test-quality-checklist.md +2 -1
- package/.cursor-plugin/marketplace.json +1 -1
- package/.github/copilot-instructions.md +14 -36
- package/.github/workflows/ci.yml +4 -4
- package/AGENTS.md +12 -3
- package/CONTRIBUTING.md +79 -0
- package/README.md +218 -129
- 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 +50 -11
- 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 +62 -17
- package/dist/contracts/execution-controls.js +24 -0
- package/dist/contracts/selector.js +8 -0
- package/dist/contracts/target.js +15 -0
- package/dist/contracts/ui.js +1 -0
- package/dist/debugger/android.js +2 -1
- package/dist/debugger/diagnostics.js +54 -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/protocol.js +33 -0
- package/dist/debugger/sessions.js +133 -74
- package/dist/debugger/worker.js +59 -46
- package/dist/interact/adjustment/adjustment-context.js +41 -0
- package/dist/interact/adjustment/adjustment-gestures.js +55 -0
- package/dist/interact/adjustment/adjustment-plan.js +18 -0
- package/dist/interact/adjustment/adjustment-resolution.js +127 -0
- package/dist/interact/adjustment/adjustment-result.js +19 -0
- package/dist/interact/adjustment/adjustment-semantic.js +64 -0
- package/dist/interact/adjustment/adjustment-verification.js +35 -0
- package/dist/interact/adjustment/adjustment.js +52 -0
- package/dist/interact/adjustment/control-geometry.js +87 -0
- package/dist/interact/adjustment/control-values.js +136 -0
- package/dist/interact/adjustment/slider-labels.js +84 -0
- package/dist/interact/assertions/screen-assertion.js +37 -0
- package/dist/interact/assertions/state-assertion-context.js +35 -0
- package/dist/interact/assertions/state-assertion.js +76 -0
- package/dist/interact/assertions/state-comparison.js +141 -0
- package/dist/interact/assertions/visibility-assertion.js +34 -0
- package/dist/interact/conditional-action.js +12 -7
- package/dist/interact/index.js +77 -3092
- package/dist/interact/input-service.js +1 -0
- package/dist/interact/interaction-trace.js +21 -0
- package/dist/interact/model.js +1 -0
- package/dist/interact/observation.js +1 -0
- package/dist/interact/targeting/candidate-ancestor.js +58 -0
- package/dist/interact/targeting/candidate-result.js +23 -0
- package/dist/interact/targeting/candidate-scoring.js +50 -0
- package/dist/interact/targeting/element-helpers.js +216 -0
- package/dist/interact/targeting/find-element.js +65 -0
- package/dist/interact/targeting/nearby-control.js +56 -0
- package/dist/interact/targeting/resolved-elements.js +21 -0
- package/dist/interact/targeting/tap-target.js +17 -0
- package/dist/interact/targeting/target-scope.js +104 -0
- package/dist/interact/waits/legacy-log-wait.js +38 -0
- package/dist/interact/waits/legacy-wait-observations.js +103 -0
- package/dist/interact/waits/legacy-wait-state.js +34 -0
- package/dist/interact/waits/legacy-wait.js +48 -0
- package/dist/interact/waits/screen-wait.js +49 -0
- package/dist/interact/waits/ui-change-helpers.js +157 -0
- package/dist/interact/waits/ui-change-state.js +18 -0
- package/dist/interact/waits/ui-change-wait.js +77 -0
- package/dist/interact/waits/ui-wait-condition.js +56 -0
- package/dist/interact/waits/ui-wait-state.js +38 -0
- package/dist/interact/waits/ui-wait.js +106 -0
- package/dist/manage/android.js +55 -51
- package/dist/manage/index.js +38 -75
- package/dist/manage/ios/build-types.js +1 -0
- package/dist/manage/ios/build.js +210 -0
- package/dist/manage/ios/project-discovery.js +29 -0
- package/dist/manage/ios/scheme.js +18 -0
- package/dist/manage/ios.js +44 -256
- package/dist/observe/android/context.js +1 -0
- package/dist/observe/android/current-screen.js +55 -0
- package/dist/observe/android/fingerprint.js +13 -0
- package/dist/observe/android/log-stream.js +132 -0
- package/dist/observe/android/logs.js +107 -0
- package/dist/observe/android/screenshot.js +70 -0
- package/dist/observe/android/ui-tree.js +107 -0
- package/dist/observe/android.js +16 -520
- package/dist/observe/evidence.js +114 -75
- package/dist/observe/index.js +16 -263
- package/dist/observe/ios/context.js +16 -0
- package/dist/observe/ios/fingerprint.js +12 -0
- package/dist/observe/ios/log-stream.js +105 -0
- package/dist/observe/ios/logs.js +168 -0
- package/dist/observe/ios/screenshot.js +22 -0
- package/dist/observe/ios/ui-elements.js +241 -0
- package/dist/observe/ios/ui-tree.js +137 -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/ios.js +17 -715
- package/dist/observe/logs/model.js +1 -0
- package/dist/{network → observe/network}/index.js +2 -2
- package/dist/observe/readiness-service.js +5 -3
- package/dist/observe/readiness.js +33 -14
- package/dist/observe/screenshot-encoding.js +41 -0
- package/dist/observe/snapshot-collection.js +79 -0
- package/dist/observe/snapshot-logs.js +30 -0
- package/dist/observe/snapshot-metadata.js +43 -31
- package/dist/observe/snapshot-semantic.js +115 -0
- package/dist/observe/system-trace-error.js +7 -0
- package/dist/observe/system-trace-router.js +81 -0
- package/dist/observe/system-trace.js +26 -21
- package/dist/runtime/action-results.js +270 -0
- package/dist/runtime/clock.js +4 -0
- package/dist/server/common.js +1 -256
- package/dist/server/definitions/assert.js +162 -0
- package/dist/server/definitions/diagnostic.js +118 -0
- package/dist/server/definitions/input.js +274 -0
- package/dist/server/definitions/manage.js +194 -0
- package/dist/server/definitions/observe.js +149 -0
- package/dist/server/definitions/target.js +138 -0
- package/dist/server/definitions/wait.js +105 -0
- package/dist/server/handlers/action-execution.js +163 -0
- package/dist/server/handlers/correlation.js +23 -0
- package/dist/server/handlers/debugger-handlers.js +67 -0
- package/dist/server/handlers/debugger-response.js +9 -0
- package/dist/server/handlers/diagnostic.js +77 -0
- package/dist/server/handlers/handler-types.js +1 -0
- package/dist/server/handlers/input.js +154 -0
- package/dist/server/handlers/interact.js +113 -0
- package/dist/server/handlers/journey-assertions.js +35 -0
- package/dist/server/handlers/journey.js +147 -0
- package/dist/server/handlers/manage.js +119 -0
- package/dist/server/handlers/observe.js +95 -0
- package/dist/server/handlers/payload.js +4 -0
- package/dist/server/handlers/tool-handlers.js +170 -0
- package/dist/server/schemas/condition-schema.js +2 -0
- package/dist/server/{debugger-schema.js → schemas/debugger-schema.js} +20 -12
- package/dist/server/{journey-schema.js → schemas/journey-schema.js} +7 -22
- package/dist/server/schemas/system-trace-schema.js +22 -0
- package/dist/server/tool-definitions.js +58 -1135
- package/dist/server-core.js +2 -2
- package/dist/server.js +4 -4
- package/dist/system/android-status.js +101 -0
- package/dist/system/android.js +37 -27
- package/dist/system/index.js +13 -439
- package/dist/system/ios-status.js +132 -0
- package/dist/system/ios.js +11 -8
- package/dist/system/model.js +1 -0
- package/dist/system/probes.js +105 -0
- package/dist/system/status-summary.js +43 -0
- package/dist/system/unsupported-status.js +52 -0
- package/dist/utils/android/commands.js +89 -0
- package/dist/utils/android/gradle.js +139 -0
- package/dist/utils/android/logs.js +103 -0
- package/dist/utils/android/ui-tree.js +233 -0
- package/dist/utils/android/utils.js +6 -564
- package/dist/utils/cli/idb/idb-helper.js +4 -6
- package/dist/utils/cli/idb/install-idb.js +13 -9
- package/dist/utils/cli/ios/preflight-ios.js +42 -38
- package/dist/utils/ios/utils.js +48 -46
- package/dist/utils/java.js +65 -49
- package/dist/utils/resolve-device.js +4 -2
- package/dist/utils/ui/index.js +26 -26
- package/docs/CHANGELOG.md +67 -1
- package/docs/README.md +38 -0
- package/docs/agent-plugin-installation-verification.md +39 -0
- package/docs/benchmarks/README.md +18 -0
- package/docs/benchmarks/interaction-latency/README.md +14 -0
- package/docs/benchmarks/interaction-latency/final.md +4 -1
- package/docs/benchmarks/interaction-latency/ios-validation.md +2 -0
- package/docs/benchmarks/ui-tree-performance/README.md +24 -16
- package/docs/benchmarks/ui-tree-performance/comparison.md +11 -10
- package/docs/code-structure.md +120 -0
- package/docs/specs/001-state-verification/spec.md +16 -14
- package/docs/specs/002-richer-element-identity/spec.md +12 -10
- package/docs/specs/003-wait-and-synchronization-reliability/spec.md +12 -10
- package/docs/specs/004-action-verification-routing/spec.md +3 -3
- package/docs/specs/005-unified-action-execution-and-verification-model/spec.md +2 -0
- package/docs/specs/006-runtime-action-instrumentation-and-binding-layer/spec.md +2 -0
- package/docs/specs/007-actionability-resolution-and-executable-target-selection/spec.md +3 -1
- package/docs/specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md +3 -4
- package/docs/specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md +2 -0
- package/docs/specs/010-verification-stabilization-and-temporal-convergence/spec.md +2 -2
- package/docs/specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md +2 -2
- package/docs/specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md +2 -0
- package/docs/specs/012-action-trace-and-xecution-observability/spec.md +2 -3
- package/docs/specs/013-wait-and-synchronization-reliability/spec.md +19 -19
- package/docs/specs/014-actionability-resolution/spec.md +20 -18
- package/docs/specs/015-adjustable-control-support/spec.md +17 -15
- package/docs/specs/016-environment-tools/spec.md +20 -20
- package/docs/specs/018-interaction-latency-and-batched-journeys/spec.md +2 -0
- package/docs/specs/019-ui-tree-observation-performance/spec.md +3 -1
- package/docs/specs/020-conditional-actions/intent.md +5 -3
- package/docs/specs/020-conditional-actions/plan.md +8 -9
- package/docs/specs/020-conditional-actions/spec.md +4 -2
- package/docs/specs/021-app-readiness-signals/intent.md +4 -2
- package/docs/specs/021-app-readiness-signals/plan.md +7 -6
- package/docs/specs/021-app-readiness-signals/spec.md +3 -1
- package/docs/specs/022-correlated-evidence/intent.md +4 -4
- package/docs/specs/022-correlated-evidence/plan.md +10 -11
- package/docs/specs/022-correlated-evidence/spec.md +3 -1
- package/docs/specs/023-android-system-traces/intent.md +3 -2
- package/docs/specs/023-android-system-traces/plan.md +5 -7
- package/docs/specs/023-android-system-traces/spec.md +3 -1
- package/docs/specs/024-android-heap-dump-capture/intent.md +3 -1
- package/docs/specs/025-android-runtime-debugger/intent.md +4 -3
- package/docs/specs/025-android-runtime-debugger/plan.md +3 -1
- package/docs/specs/025-android-runtime-debugger/spec.md +4 -3
- package/docs/specs/026-code-structure-and-quality-gates/health-plan.md +88 -0
- package/docs/specs/026-code-structure-and-quality-gates/intent.md +47 -0
- package/docs/specs/026-code-structure-and-quality-gates/plan.md +191 -0
- package/docs/specs/026-code-structure-and-quality-gates/spec.md +138 -0
- package/docs/specs/026-code-structure-and-quality-gates/verification-final.json +200 -0
- package/docs/specs/026-code-structure-and-quality-gates/verification-thin-facade.json +154 -0
- package/docs/specs/026-code-structure-and-quality-gates/verification.json +136 -0
- package/docs/specs/027-debugger-failure-diagnostics/intent.md +43 -0
- package/docs/specs/027-debugger-failure-diagnostics/plan.md +86 -0
- package/docs/specs/027-debugger-failure-diagnostics/spec.md +133 -0
- package/docs/specs/028-compact-debugger-responses/intent.md +41 -0
- package/docs/specs/028-compact-debugger-responses/plan.md +112 -0
- package/docs/specs/028-compact-debugger-responses/spec.md +108 -0
- package/docs/specs/029-kotlin-locals-validation/report.md +119 -0
- package/docs/specs/030-android-coroutine-validation/evidence.json +475 -0
- package/docs/specs/030-android-coroutine-validation/intent.md +47 -0
- package/docs/specs/030-android-coroutine-validation/plan.md +147 -0
- package/docs/specs/030-android-coroutine-validation/report.md +240 -0
- package/docs/specs/030-android-coroutine-validation/spec.md +107 -0
- 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 +31 -18
- package/docs/tools/{evidence.md → correlated-evidence.md} +9 -1
- package/docs/tools/debugger.md +120 -4
- package/docs/tools/interact.md +42 -20
- package/docs/tools/manage.md +25 -44
- package/docs/tools/observe.md +55 -23
- package/docs/tools/system-traces.md +89 -14
- package/docs/tools/system.md +12 -5
- package/package.json +12 -8
- package/scripts/agent-plugin.mjs +55 -5
- package/scripts/architecture-graph.mjs +69 -0
- package/scripts/check-architecture.mjs +47 -0
- package/scripts/code-quality-baseline.json +1 -0
- package/scripts/code-quality.mjs +161 -0
- package/src/contracts/execution-controls.ts +26 -0
- package/src/contracts/selector.ts +17 -0
- package/src/contracts/target.ts +19 -0
- package/src/contracts/ui.ts +94 -0
- package/src/debugger/android.ts +3 -2
- package/src/debugger/diagnostics.ts +43 -0
- package/src/debugger/guard.ts +10 -5
- package/src/debugger/ios-worker.ts +49 -0
- package/src/debugger/ios.ts +45 -0
- package/{worker → src/debugger/java}/DebuggerWorker.java +9 -3
- 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/protocol.ts +37 -0
- package/src/debugger/sessions.ts +326 -155
- package/src/debugger/worker.ts +57 -41
- package/src/interact/adjustment/adjustment-context.ts +83 -0
- package/src/interact/adjustment/adjustment-gestures.ts +48 -0
- package/src/interact/adjustment/adjustment-plan.ts +23 -0
- package/src/interact/adjustment/adjustment-resolution.ts +104 -0
- package/src/interact/adjustment/adjustment-result.ts +34 -0
- package/src/interact/adjustment/adjustment-semantic.ts +53 -0
- package/src/interact/adjustment/adjustment-verification.ts +60 -0
- package/src/interact/adjustment/adjustment.ts +49 -0
- package/src/interact/adjustment/control-geometry.ts +120 -0
- package/src/interact/adjustment/control-values.ts +141 -0
- package/src/interact/adjustment/slider-labels.ts +84 -0
- package/src/interact/assertions/screen-assertion.ts +39 -0
- package/src/interact/assertions/state-assertion-context.ts +48 -0
- package/src/interact/assertions/state-assertion.ts +64 -0
- package/src/interact/assertions/state-comparison.ts +168 -0
- package/src/interact/assertions/visibility-assertion.ts +41 -0
- package/src/interact/conditional-action.ts +18 -12
- package/src/interact/index.ts +89 -3494
- package/src/interact/input-service.ts +11 -0
- package/src/interact/interaction-trace.ts +36 -0
- package/src/interact/model.ts +109 -0
- package/src/interact/observation.ts +7 -0
- package/src/interact/targeting/candidate-ancestor.ts +49 -0
- package/src/interact/targeting/candidate-result.ts +24 -0
- package/src/interact/targeting/candidate-scoring.ts +42 -0
- package/src/interact/targeting/element-helpers.ts +243 -0
- package/src/interact/targeting/find-element.ts +47 -0
- package/src/interact/targeting/nearby-control.ts +46 -0
- package/src/interact/targeting/resolved-elements.ts +20 -0
- package/src/interact/targeting/tap-target.ts +22 -0
- package/src/interact/targeting/target-scope.ts +121 -0
- package/src/interact/waits/legacy-log-wait.ts +30 -0
- package/src/interact/waits/legacy-wait-observations.ts +55 -0
- package/src/interact/waits/legacy-wait-state.ts +48 -0
- package/src/interact/waits/legacy-wait.ts +34 -0
- package/src/interact/waits/screen-wait.ts +37 -0
- package/src/interact/waits/ui-change-helpers.ts +184 -0
- package/src/interact/waits/ui-change-state.ts +26 -0
- package/src/interact/waits/ui-change-wait.ts +59 -0
- package/src/interact/waits/ui-wait-condition.ts +52 -0
- package/src/interact/waits/ui-wait-state.ts +55 -0
- package/src/interact/waits/ui-wait.ts +84 -0
- package/src/manage/android.ts +56 -50
- package/src/manage/index.ts +63 -93
- package/src/manage/ios/build-types.ts +14 -0
- package/src/manage/ios/build.ts +230 -0
- package/src/manage/ios/project-discovery.ts +26 -0
- package/src/manage/ios/scheme.ts +16 -0
- package/src/manage/ios.ts +67 -298
- package/src/observe/android/context.ts +1 -0
- package/src/observe/android/current-screen.ts +63 -0
- package/src/observe/android/fingerprint.ts +13 -0
- package/src/observe/android/log-stream.ts +127 -0
- package/src/observe/android/logs.ts +110 -0
- package/src/observe/android/screenshot.ts +79 -0
- package/src/observe/android/ui-tree.ts +113 -0
- package/src/observe/android.ts +17 -531
- package/src/observe/evidence.ts +122 -70
- package/src/observe/index.ts +24 -286
- package/src/observe/ios/context.ts +22 -0
- package/src/observe/ios/fingerprint.ts +12 -0
- package/src/observe/ios/log-stream.ts +103 -0
- package/src/observe/ios/logs.ts +179 -0
- package/src/observe/ios/screenshot.ts +26 -0
- package/src/observe/ios/ui-elements.ts +266 -0
- package/src/observe/ios/ui-tree.ts +143 -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/ios.ts +18 -749
- package/src/observe/logs/model.ts +11 -0
- package/src/{network → observe/network}/index.ts +2 -2
- package/src/observe/readiness-service.ts +6 -4
- package/src/observe/readiness.ts +42 -21
- package/src/observe/screenshot-encoding.ts +32 -0
- package/src/observe/snapshot-collection.ts +69 -0
- package/src/observe/snapshot-logs.ts +26 -0
- package/src/observe/snapshot-metadata.ts +51 -31
- package/src/observe/snapshot-semantic.ts +153 -0
- package/src/observe/system-trace-error.ts +3 -0
- package/src/observe/system-trace-router.ts +61 -0
- package/src/observe/system-trace.ts +28 -19
- package/src/runtime/action-results.ts +376 -0
- package/src/runtime/clock.ts +9 -0
- package/src/server/common.ts +9 -359
- package/src/server/definitions/assert.ts +164 -0
- package/src/server/definitions/diagnostic.ts +120 -0
- package/src/server/definitions/input.ts +276 -0
- package/src/server/definitions/manage.ts +196 -0
- package/src/server/definitions/observe.ts +151 -0
- package/src/server/definitions/target.ts +140 -0
- package/src/server/definitions/wait.ts +107 -0
- package/src/server/handlers/action-execution.ts +164 -0
- package/src/server/handlers/correlation.ts +20 -0
- package/src/server/handlers/debugger-handlers.ts +58 -0
- package/src/server/handlers/debugger-response.ts +7 -0
- package/src/server/handlers/diagnostic.ts +86 -0
- package/src/server/handlers/handler-types.ts +7 -0
- package/src/server/handlers/input.ts +173 -0
- package/src/server/handlers/interact.ts +135 -0
- package/src/server/handlers/journey-assertions.ts +39 -0
- package/src/server/handlers/journey.ts +132 -0
- package/src/server/handlers/manage.ts +141 -0
- package/src/server/handlers/observe.ts +116 -0
- package/src/server/handlers/payload.ts +36 -0
- package/src/server/handlers/tool-handlers.ts +170 -0
- package/src/server/schemas/condition-schema.ts +2 -0
- package/src/server/schemas/debugger-schema.ts +46 -0
- package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +7 -22
- package/src/server/schemas/system-trace-schema.ts +23 -0
- package/src/server/tool-definitions.ts +67 -1143
- package/src/server-core.ts +2 -2
- package/src/server.ts +4 -4
- package/src/system/android-status.ts +124 -0
- package/src/system/android.ts +33 -21
- package/src/system/index.ts +21 -525
- package/src/system/ios-status.ts +147 -0
- package/src/system/ios.ts +14 -10
- package/src/system/model.ts +41 -0
- package/src/system/probes.ts +114 -0
- package/src/system/status-summary.ts +47 -0
- package/src/system/unsupported-status.ts +55 -0
- package/src/types.ts +16 -106
- package/src/utils/android/commands.ts +81 -0
- package/src/utils/android/gradle.ts +132 -0
- package/src/utils/android/logs.ts +103 -0
- package/src/utils/android/ui-tree.ts +239 -0
- package/src/utils/android/utils.ts +16 -556
- package/src/utils/cli/idb/check-idb.ts +4 -4
- package/src/utils/cli/idb/idb-helper.ts +6 -8
- package/src/utils/cli/idb/install-idb.ts +15 -9
- package/src/utils/cli/ios/preflight-ios.ts +39 -31
- package/src/utils/cli/ios/run-ios-ui-tree-tap.ts +4 -4
- package/src/utils/ios/utils.ts +60 -61
- package/src/utils/java.ts +62 -43
- package/src/utils/resolve-device.ts +9 -6
- package/src/utils/ui/index.ts +28 -28
- package/test/device/automated/debugger/coroutine-lifecycle.ts +27 -0
- package/test/device/automated/debugger/coroutine-support.ts +140 -0
- package/test/device/automated/debugger/coroutine.android.test.ts +169 -0
- package/test/device/automated/debugger/kotlin.android.test.ts +127 -0
- package/test/device/automated/debugger/runtime.android.test.ts +19 -3
- package/test/device/automated/debugger/runtime.ios.test.ts +221 -0
- package/test/device/automated/interact/interaction.android.test.ts +56 -0
- package/test/device/automated/observe/ios-system-trace.smoke.ts +119 -0
- package/test/device/automated/observe/system-trace.smoke.ts +1 -1
- package/test/device/manual/interact/modul8_selector_tap.manual.ts +1 -1
- package/test/fixtures/contracts/tool-definitions.json +3696 -0
- package/test/fixtures/coroutine-app/MainActivity.kt +100 -0
- package/test/fixtures/coroutine-app/README.md +48 -0
- package/test/fixtures/coroutine-app/build.ts +35 -0
- package/test/fixtures/debugger-app/MainActivity.java +21 -0
- package/test/fixtures/debugger-app/MainActivity.kt +32 -0
- package/test/fixtures/debugger-app/README.md +52 -0
- package/test/fixtures/debugger-app/build.ts +25 -15
- 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/README.md +2 -0
- package/test/fixtures/readiness-app/README.md +2 -0
- package/test/helpers/ios-trace-shutdown.ts +29 -0
- package/test/unit/debugger/coroutine-lifecycle.test.ts +43 -0
- package/test/unit/debugger/diagnostic-sessions.test.ts +67 -0
- package/test/unit/debugger/diagnostics.test.ts +80 -0
- package/test/unit/debugger/guards.test.ts +4 -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/debugger/preparation.test.ts +32 -0
- package/test/unit/debugger/protocol.test.ts +15 -0
- package/test/unit/debugger/sessions.test.ts +1 -1
- package/test/unit/interact/adjustment_service.test.ts +42 -0
- package/test/unit/interact/adjustment_verification.test.ts +35 -0
- package/test/unit/interact/clock_dependencies.test.ts +63 -0
- package/test/unit/interact/element_helpers.test.ts +19 -0
- package/test/unit/interact/legacy_wait_service.test.ts +17 -0
- package/test/unit/interact/state_comparison.test.ts +20 -0
- package/test/unit/interact/target_scope.test.ts +35 -0
- package/test/unit/network/get_network_activity.test.ts +1 -1
- 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/screenshot-encoding.test.ts +30 -0
- package/test/unit/observe/snapshot-logs.test.ts +14 -0
- 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/quality/architecture.test.ts +60 -0
- package/test/unit/quality/code-quality.test.ts +58 -0
- package/test/unit/quality/interaction_boundaries.test.ts +24 -0
- package/test/unit/server/conditional_actions.test.ts +1 -1
- package/test/unit/server/conditional_schema.test.ts +2 -2
- package/test/unit/server/debugger-response.test.ts +63 -0
- package/test/unit/server/system-trace.test.ts +12 -3
- package/test/unit/server/tool_definitions_parity.test.ts +8 -0
- package/dist/server/debugger-handlers.js +0 -38
- package/dist/server/system-trace-schema.js +0 -17
- package/dist/server/tool-handlers.js +0 -1000
- package/docs/ROADMAP.md +0 -646
- package/docs/benchmarks/ui-tree-performance/bridge-probe.json +0 -29
- package/docs/benchmarks/ui-tree-performance/initial-baseline.json +0 -59560
- package/docs/specs/baseline-spec-v0.md +0 -312
- package/docs/specs/mcp-tooling-spec-v1.md +0 -446
- package/src/server/debugger-handlers.ts +0 -29
- package/src/server/debugger-schema.ts +0 -37
- package/src/server/system-trace-schema.ts +0 -17
- package/src/server/tool-handlers.ts +0 -1008
- /package/dist/{server/condition-schema.js → contracts/condition.js} +0 -0
- /package/dist/server/{readiness-schema.js → schemas/readiness-schema.js} +0 -0
- /package/src/{server/condition-schema.ts → contracts/condition.ts} +0 -0
- /package/src/server/{readiness-schema.ts → schemas/readiness-schema.ts} +0 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { remainingBudget } from '../../utils/operation-budget.js'
|
|
2
|
+
import { execCmd } from '../../utils/exec.js'
|
|
3
|
+
import { createObservationMeasurement } from '../performance-metrics.js'
|
|
4
|
+
import { GetUITreeResponse, UIElement } from "../../types.js"
|
|
5
|
+
import { getIOSDeviceMetadata, getIdbCmd, isIDBInstalled } from "../../utils/ios/utils.js"
|
|
6
|
+
import { deriveSnapshotMetadata } from '../snapshot-metadata.js'
|
|
7
|
+
import { delay } from './context.js'
|
|
8
|
+
import { traverseIDBNode } from './ui-elements.js'
|
|
9
|
+
export async function collectGetUITree(deviceId: string = "booted"): Promise<GetUITreeResponse> {
|
|
10
|
+
const measurement = createObservationMeasurement('ios')
|
|
11
|
+
let device
|
|
12
|
+
try {
|
|
13
|
+
device = await measurement.timed('metadata_ms', () => getIOSDeviceMetadata(deviceId))
|
|
14
|
+
} catch (error) {
|
|
15
|
+
measurement.setOutcome('unavailable')
|
|
16
|
+
measurement.finish()
|
|
17
|
+
throw error
|
|
18
|
+
}
|
|
19
|
+
const deviceKey = `ios:${device.id}`
|
|
20
|
+
|
|
21
|
+
const idbExists = await measurement.timed('capability_check_ms', () => isIDBInstalled());
|
|
22
|
+
if (!idbExists) {
|
|
23
|
+
const snapshotMetadata = deriveSnapshotMetadata(deviceKey, null, 'ui_tree')
|
|
24
|
+
measurement.setOutcome('unavailable')
|
|
25
|
+
measurement.finish()
|
|
26
|
+
return missingIDBResponse(device, snapshotMetadata)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const targetUdid = (device.id && device.id !== 'booted') ? device.id : undefined;
|
|
30
|
+
|
|
31
|
+
const acquisition = await readIOSTree(targetUdid, measurement)
|
|
32
|
+
if (!acquisition.success) {
|
|
33
|
+
const snapshotMetadata = deriveSnapshotMetadata(deviceKey, null, 'ui_tree')
|
|
34
|
+
measurement.setOutcome('unavailable'); measurement.finish()
|
|
35
|
+
return {
|
|
36
|
+
device, screen: '', resolution: { width: 0, height: 0 }, elements: [], ...snapshotMetadata,
|
|
37
|
+
error: 'Failed to retrieve valid UI dump after 3 attempts.'
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const jsonContent = acquisition.tree
|
|
42
|
+
try {
|
|
43
|
+
const elements: UIElement[] = await measurement.timed('conversion_ms', async () => {
|
|
44
|
+
const parsed: UIElement[] = []
|
|
45
|
+
if (Array.isArray(jsonContent)) {
|
|
46
|
+
for (const node of jsonContent) traverseIDBNode(node, parsed)
|
|
47
|
+
} else {
|
|
48
|
+
traverseIDBNode(jsonContent!, parsed)
|
|
49
|
+
}
|
|
50
|
+
return parsed
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
let width = 0;
|
|
54
|
+
let height = 0;
|
|
55
|
+
if (elements.length > 0) {
|
|
56
|
+
const rootBounds = elements[0].bounds;
|
|
57
|
+
width = rootBounds[2] - rootBounds[0];
|
|
58
|
+
height = rootBounds[3] - rootBounds[1];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const snapshotMetadata = deriveSnapshotMetadata(deviceKey, {
|
|
62
|
+
screen: "",
|
|
63
|
+
resolution: { width, height },
|
|
64
|
+
elements
|
|
65
|
+
}, 'ui_tree')
|
|
66
|
+
|
|
67
|
+
measurement.setOutcome('observed')
|
|
68
|
+
const response = measurement.bindObservation({
|
|
69
|
+
device,
|
|
70
|
+
screen: "",
|
|
71
|
+
resolution: { width, height },
|
|
72
|
+
elements,
|
|
73
|
+
...snapshotMetadata
|
|
74
|
+
})
|
|
75
|
+
measurement.finish()
|
|
76
|
+
return response
|
|
77
|
+
} catch (e) {
|
|
78
|
+
const snapshotMetadata = deriveSnapshotMetadata(deviceKey, null, 'ui_tree')
|
|
79
|
+
measurement.setOutcome('unavailable')
|
|
80
|
+
measurement.finish()
|
|
81
|
+
return {
|
|
82
|
+
device,
|
|
83
|
+
screen: "",
|
|
84
|
+
resolution: { width: 0, height: 0 },
|
|
85
|
+
elements: [],
|
|
86
|
+
...snapshotMetadata,
|
|
87
|
+
error: `Failed to parse idb output: ${e instanceof Error ? e.message : String(e)}`
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function readIOSTree(targetUdid: string | undefined, measurement: ReturnType<typeof createObservationMeasurement>) {
|
|
93
|
+
let jsonContent: import('./ui-elements.js').IDBElement | import('./ui-elements.js').IDBElement[] | null = null;
|
|
94
|
+
let attempts = 0;
|
|
95
|
+
const maxAttempts = 3;
|
|
96
|
+
|
|
97
|
+
while (attempts < maxAttempts) {
|
|
98
|
+
remainingBudget()
|
|
99
|
+
attempts++;
|
|
100
|
+
try {
|
|
101
|
+
await measurement.timed('observation_wait_ms', () => delay(Math.min(remainingBudget(300 + (attempts * 100)), 300 + (attempts * 100))));
|
|
102
|
+
remainingBudget()
|
|
103
|
+
|
|
104
|
+
const args = ['ui', 'describe-all', '--json'];
|
|
105
|
+
if (targetUdid) {
|
|
106
|
+
args.push('--udid', targetUdid);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const output = await measurement.physicalRead(async () => {
|
|
110
|
+
const result = await execCmd(getIdbCmd(), args, { timeout: remainingBudget() })
|
|
111
|
+
if (result.stderr === 'ACTION_TIMEOUT') throw new Error('ACTION_TIMEOUT')
|
|
112
|
+
if (result.exitCode !== 0) throw new Error(`idb failed (code ${result.exitCode}): ${result.stderr}`)
|
|
113
|
+
if (!result.stdout.trim()) throw new Error('Empty idb UI tree output')
|
|
114
|
+
return result.stdout
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
if (output && output.trim().length > 0) {
|
|
118
|
+
jsonContent = await measurement.timed('parse_ms', async () => JSON.parse(output));
|
|
119
|
+
break; // Success
|
|
120
|
+
}
|
|
121
|
+
} catch (e) {
|
|
122
|
+
if (e instanceof Error && e.message === 'ACTION_TIMEOUT') throw e
|
|
123
|
+
console.error(`Attempt ${attempts} failed: ${e}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (attempts === maxAttempts) {
|
|
127
|
+
return { success: false, tree: null };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { success: true, tree: jsonContent }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function missingIDBResponse(device: GetUITreeResponse['device'], snapshotMetadata: ReturnType<typeof deriveSnapshotMetadata>): GetUITreeResponse {
|
|
135
|
+
return {
|
|
136
|
+
device,
|
|
137
|
+
screen: "",
|
|
138
|
+
resolution: { width: 0, height: 0 },
|
|
139
|
+
elements: [],
|
|
140
|
+
...snapshotMetadata,
|
|
141
|
+
error: "iOS UI tree retrieval requires 'idb' (iOS Device Bridge). Please install it via Homebrew: `brew tap facebook/fb && brew install idb-companion` and `pip3 install fb-idb`."
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { XMLParser, XMLValidator } from 'fast-xml-parser'
|
|
4
|
+
import { getXcrunCmd } from '../../utils/ios/utils.js'
|
|
5
|
+
import { traceCommand, spawnTrace, type TraceSpawn } from './process.js'
|
|
6
|
+
import type { IosApp } from './capabilities.js'
|
|
7
|
+
|
|
8
|
+
export const IOS_TRACE_MAX_BYTES = 256 * 1024 * 1024
|
|
9
|
+
export async function traceSize(root: string, secure = false): Promise<number> {
|
|
10
|
+
const stat = await fs.lstat(root)
|
|
11
|
+
if (stat.isSymbolicLink() || (!stat.isFile() && !stat.isDirectory())) throw new Error('UNSAFE_ARTIFACT')
|
|
12
|
+
if (secure) await fs.chmod(root, stat.isDirectory() ? 0o700 : 0o600)
|
|
13
|
+
if (stat.isFile()) return stat.size
|
|
14
|
+
let size = 0
|
|
15
|
+
for (const name of await fs.readdir(root)) {
|
|
16
|
+
size += await traceSize(path.join(root, name), secure)
|
|
17
|
+
if (size > IOS_TRACE_MAX_BYTES) throw new Error('STORAGE_LIMIT')
|
|
18
|
+
}
|
|
19
|
+
return size
|
|
20
|
+
}
|
|
21
|
+
interface TocProcess { '@_pid'?: string; '@_path'?: string }
|
|
22
|
+
interface TocRun {
|
|
23
|
+
processes?: { process?: TocProcess | TocProcess[] }
|
|
24
|
+
info?: { target?: { device?: { '@_uuid'?: string }; process?: { '@_pid'?: string; '@_name'?: string } }; summary?: { 'template-name'?: string } }
|
|
25
|
+
data?: { table?: { '@_schema'?: string }[] | { '@_schema'?: string } }
|
|
26
|
+
}
|
|
27
|
+
export function validateToc(xml: string, device: string, app: IosApp) {
|
|
28
|
+
if (XMLValidator.validate(xml) !== true) throw new Error('INVALID_TOC')
|
|
29
|
+
const parsed = new XMLParser({ ignoreAttributes: false }).parse(xml) as { 'trace-toc'?: { run?: TocRun | TocRun[] } }
|
|
30
|
+
const raw = parsed['trace-toc']?.run
|
|
31
|
+
if (!raw || Array.isArray(raw)) throw new Error('INVALID_TOC')
|
|
32
|
+
const target = raw.info?.target
|
|
33
|
+
if (target?.device?.['@_uuid'] !== device || Number(target.process?.['@_pid']) !== app.pid || raw.info?.summary?.['template-name'] !== 'Time Profiler') throw new Error('ARTIFACT_TARGET_MISMATCH')
|
|
34
|
+
const processes = raw.processes?.process
|
|
35
|
+
if (!(Array.isArray(processes) ? processes : [processes]).some(p => Number(p?.['@_pid']) === app.pid && p?.['@_path'] === app.executable)) throw new Error('ARTIFACT_APP_MISMATCH')
|
|
36
|
+
const tables = raw.data?.table
|
|
37
|
+
if (!(Array.isArray(tables) ? tables : [tables]).some(t => t?.['@_schema'] === 'time-sample')) throw new Error('SAMPLE_SCHEMA_MISSING')
|
|
38
|
+
}
|
|
39
|
+
export async function validateArtifact(file: string, device: string, app: IosApp, spawn: TraceSpawn = spawnTrace) {
|
|
40
|
+
const size = await traceSize(file, true)
|
|
41
|
+
if (size <= 0 || size > IOS_TRACE_MAX_BYTES) throw new Error('STORAGE_LIMIT')
|
|
42
|
+
const toc = path.join(path.dirname(file), 'toc.xml'), samples = path.join(path.dirname(file), 'samples.xml')
|
|
43
|
+
try {
|
|
44
|
+
await traceCommand(getXcrunCmd(), ['xctrace', 'export', '--input', file, '--toc', '--output', toc], 15000, spawn)
|
|
45
|
+
if ((await fs.stat(toc)).size > 1024 * 1024) throw new Error('INVALID_TOC')
|
|
46
|
+
validateToc(await fs.readFile(toc, 'utf8'), device, app)
|
|
47
|
+
await traceCommand(getXcrunCmd(), ['xctrace', 'export', '--input', file, '--xpath', '/trace-toc/run[@number="1"]/data/table[@schema="time-sample"]', '--output', samples], 15000, spawn)
|
|
48
|
+
if ((await fs.stat(samples)).size > IOS_TRACE_MAX_BYTES) throw new Error('STORAGE_LIMIT')
|
|
49
|
+
const xml = await fs.readFile(samples, 'utf8')
|
|
50
|
+
if (XMLValidator.validate(xml) !== true || !xml.includes('<trace-query-result>')) throw new Error('INVALID_SAMPLES')
|
|
51
|
+
const sample_rows = (xml.match(/<row(?:\s|>)/g) ?? []).length
|
|
52
|
+
return { path: file, format: 'instruments' as const, kind: (await fs.stat(file)).isDirectory() ? 'directory' as const : 'file' as const,
|
|
53
|
+
size_bytes: size, validation: { readable: true, schema: 'time-sample', sample_rows } }
|
|
54
|
+
} finally { await Promise.all([toc, samples].map(p => fs.rm(p, { force: true }))) }
|
|
55
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { getXcrunCmd } from '../../utils/ios/utils.js'
|
|
5
|
+
import { traceCommand, type TraceSpawn, spawnTrace } from './process.js'
|
|
6
|
+
import { prepareNotification } from './notification.js'
|
|
7
|
+
|
|
8
|
+
export const IOS_TEMPLATES = ['System Trace', 'Time Profiler', 'Animation Hitches', 'Hangs'] as const
|
|
9
|
+
export type IosTemplate = typeof IOS_TEMPLATES[number]
|
|
10
|
+
export interface IosTarget { id: string; platform: 'ios'; simulator: boolean; osVersion: string; model: string }
|
|
11
|
+
export interface IosApp { app_id: string; pid: number; executable: string; started_at: string }
|
|
12
|
+
export interface IosCapabilities {
|
|
13
|
+
supported: boolean; backend: 'xctrace'; device: IosTarget | null; xcode_version: string | null
|
|
14
|
+
prerequisites: Record<string, 'available' | 'missing' | 'unknown'>; reasons: string[]; recovery: string[]
|
|
15
|
+
discovered_templates: string[]; templates: { name: IosTemplate; supported: boolean; coverage: string; reason: string | null }[]
|
|
16
|
+
app: IosApp | null; limitations: string[]
|
|
17
|
+
}
|
|
18
|
+
interface Simulators { devices: Record<string, { udid: string; state: string; name: string; isAvailable: boolean }[]> }
|
|
19
|
+
export async function resolveIosApp(device: string, appId: string, spawn: TraceSpawn = spawnTrace): Promise<IosApp> {
|
|
20
|
+
const run = (args: string[]) => traceCommand(getXcrunCmd(), args, 10000, spawn)
|
|
21
|
+
const container = await run(['simctl', 'get_app_container', device, appId, 'app'])
|
|
22
|
+
if (!path.isAbsolute(container) || !container.endsWith('.app')) throw new Error('APP_IDENTITY_UNAVAILABLE')
|
|
23
|
+
const output = await run(['simctl', 'spawn', device, 'launchctl', 'list'])
|
|
24
|
+
const ids = output.split('\n').filter(line => line.includes(`UIKitApplication:${appId}[`))
|
|
25
|
+
.map(line => Number(line.trim().split(/\s+/)[0])).filter(pid => Number.isSafeInteger(pid) && pid > 1)
|
|
26
|
+
if (ids.length !== 1) throw new Error('APP_NOT_RUNNING')
|
|
27
|
+
const executable = await traceCommand('/bin/ps', ['-p', String(ids[0]), '-o', 'comm='], 10000, spawn)
|
|
28
|
+
if (!executable.startsWith(container + '/') || executable.includes('\n')) throw new Error('APP_IDENTITY_UNAVAILABLE')
|
|
29
|
+
const started_at = await traceCommand('/bin/ps', ['-p', String(ids[0]), '-o', 'lstart='], 10000, spawn)
|
|
30
|
+
if (!started_at || started_at.includes('\n')) throw new Error('APP_IDENTITY_UNAVAILABLE')
|
|
31
|
+
return { app_id: appId, pid: ids[0], executable, started_at }
|
|
32
|
+
}
|
|
33
|
+
export async function iosCapabilities(device: string, appId?: string, template: IosTemplate = 'Time Profiler', spawn: TraceSpawn = spawnTrace, host: string = process.platform): Promise<IosCapabilities> {
|
|
34
|
+
const result: IosCapabilities = {
|
|
35
|
+
supported: false, backend: 'xctrace', device: null, xcode_version: null,
|
|
36
|
+
prerequisites: { host: host === 'darwin' ? 'available' : 'missing', permissions: 'unknown', symbols: 'unknown' },
|
|
37
|
+
reasons: [], recovery: [], discovered_templates: [], templates: [], app: null,
|
|
38
|
+
limitations: ['App attachment may include sensitive system metadata, paths, logs and signposts.', 'Permissions and symbolication are not established by discovery.', 'Physical iOS capture is not supported in the initial matrix.']
|
|
39
|
+
}
|
|
40
|
+
if (host !== 'darwin') { result.reasons.push('MACOS_REQUIRED'); return result }
|
|
41
|
+
const probe = async (key: string, fn: () => Promise<void>, recovery: string) => {
|
|
42
|
+
try { await fn(); result.prerequisites[key] = 'available' }
|
|
43
|
+
catch (error) { result.prerequisites[key] = 'missing'; result.reasons.push(error instanceof Error && ['APP_NOT_RUNNING', 'APP_IDENTITY_UNAVAILABLE'].includes(error.message) ? error.message : key.toUpperCase() + '_UNAVAILABLE'); result.recovery.push(recovery) }
|
|
44
|
+
}
|
|
45
|
+
await probe('xcode', async () => { result.xcode_version = await traceCommand(getXcrunCmd(), ['xcodebuild', '-version'], 10000, spawn) }, 'Select a full Xcode installation with xcode-select.')
|
|
46
|
+
await probe('templates', async () => {
|
|
47
|
+
const output = await traceCommand(getXcrunCmd(), ['xctrace', 'list', 'templates'], 10000, spawn)
|
|
48
|
+
result.discovered_templates = output.split('\n').map(s => s.trim()).filter(s => s && !s.startsWith('=='))
|
|
49
|
+
}, 'Install/select Xcode with Instruments and run xcrun xctrace list templates.')
|
|
50
|
+
await probe('instruments_targets', async () => {
|
|
51
|
+
const targets = await traceCommand(getXcrunCmd(), ['xctrace', 'list', 'devices'], 15000, spawn)
|
|
52
|
+
if (!targets.includes(`(${device})`)) throw new Error('TARGET_UNAVAILABLE')
|
|
53
|
+
}, 'Check xcrun xctrace list devices; use a simulator visible to Instruments.')
|
|
54
|
+
await probe('target', async () => {
|
|
55
|
+
const data = JSON.parse(await traceCommand(getXcrunCmd(), ['simctl', 'list', 'devices', '--json'], 10000, spawn)) as Simulators
|
|
56
|
+
const entries = Object.entries(data.devices).flatMap(([runtime, values]) => values.map(value => ({ ...value, runtime })))
|
|
57
|
+
const target = entries.find(d => d.udid === device)
|
|
58
|
+
if (!target || !target.isAvailable || target.state !== 'Booted') throw new Error('TARGET_UNAVAILABLE')
|
|
59
|
+
result.device = { platform: 'ios', id: target.udid, simulator: true, model: target.name, osVersion: target.runtime.split('iOS-')[1]?.replaceAll('-', '.') ?? target.runtime }
|
|
60
|
+
}, 'Use an explicit booted simulator UDID. Physical devices and the Mac host are unsupported.')
|
|
61
|
+
await probe('notification_helper', async () => {
|
|
62
|
+
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-trace-probe-'))
|
|
63
|
+
try { await prepareNotification(directory, spawn) } finally { await fs.rm(directory, { recursive: true, force: true }) }
|
|
64
|
+
}, 'Allow Xcode clang to compile the local Darwin-notification listener.')
|
|
65
|
+
if (appId && result.device) await probe('app', async () => { result.app = await resolveIosApp(device, appId, spawn) }, 'Start the requested development app on the selected simulator before tracing.')
|
|
66
|
+
result.templates = IOS_TEMPLATES.map(name => ({ name,
|
|
67
|
+
supported: name === 'Time Profiler' && result.discovered_templates.includes(name) && result.device !== null,
|
|
68
|
+
coverage: name === 'Time Profiler' ? 'CPU samples; bundled Hangs instrument is not independently verified' : name === 'System Trace' ? 'Scheduling coverage unverified' : name === 'Hangs' ? 'Not a standalone template here' : 'Unsupported on tested simulator',
|
|
69
|
+
reason: name === 'Time Profiler' ? null : 'TEMPLATE_TARGET_UNSUPPORTED'
|
|
70
|
+
}))
|
|
71
|
+
if (!result.templates.find(t => t.name === template)?.supported) result.reasons.push('TEMPLATE_TARGET_UNSUPPORTED')
|
|
72
|
+
result.supported = result.reasons.length === 0
|
|
73
|
+
return result
|
|
74
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs'
|
|
2
|
+
import { randomUUID } from 'node:crypto'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { getXcrunCmd } from '../../utils/ios/utils.js'
|
|
5
|
+
import { iosCapabilities, resolveIosApp, type IosApp, type IosTemplate } from './capabilities.js'
|
|
6
|
+
import { prepareNotification, listenNotification } from './notification.js'
|
|
7
|
+
import { spawnTrace, stopTraceChild, classifyTraceFailure, type TraceSpawn, type OwnedTraceChild } from './process.js'
|
|
8
|
+
import { traceSize, validateArtifact, IOS_TRACE_MAX_BYTES } from './artifact.js'
|
|
9
|
+
|
|
10
|
+
export interface IosRecording { recorder: OwnedTraceChild; file: string; ready_at_ms: number }
|
|
11
|
+
export class IosTraceNative {
|
|
12
|
+
private spawn: TraceSpawn
|
|
13
|
+
private children = new Set<OwnedTraceChild>()
|
|
14
|
+
private recorders = new Set<OwnedTraceChild>()
|
|
15
|
+
private closing = false
|
|
16
|
+
constructor(spawn: TraceSpawn = spawnTrace) {
|
|
17
|
+
this.spawn = (command, args, timeout) => {
|
|
18
|
+
if (this.closing) throw new Error('TRACE_BUSY')
|
|
19
|
+
const child = spawn(command, args, timeout)
|
|
20
|
+
this.children.add(child)
|
|
21
|
+
if (args.includes('xctrace') && args.includes('record')) this.recorders.add(child)
|
|
22
|
+
void child.done.then(() => { this.children.delete(child); this.recorders.delete(child) })
|
|
23
|
+
return child
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
shutdown() {
|
|
27
|
+
this.closing = true
|
|
28
|
+
for (const child of this.children) child.signal('SIGINT')
|
|
29
|
+
const helpers = setTimeout(() => { for (const child of this.children) if (!this.recorders.has(child)) child.signal('SIGKILL') }, 1000)
|
|
30
|
+
const recorders = setTimeout(() => { for (const child of this.recorders) child.signal('SIGKILL') }, 8000)
|
|
31
|
+
helpers.unref(); recorders.unref()
|
|
32
|
+
}
|
|
33
|
+
capabilities(device: string, appId?: string, template?: IosTemplate) { return iosCapabilities(device, appId, template, this.spawn) }
|
|
34
|
+
resolve(device: string, app: string) { return resolveIosApp(device, app, this.spawn) }
|
|
35
|
+
async storage(directory: string) {
|
|
36
|
+
const stat = await fs.statfs(directory)
|
|
37
|
+
if (stat.bavail * stat.bsize < 1024 * 1024 * 1024) throw new Error('STORAGE_LIMIT')
|
|
38
|
+
}
|
|
39
|
+
async start(device: string, app: IosApp, directory: string, duration: number, own: (recording: IosRecording) => void): Promise<IosRecording> {
|
|
40
|
+
await this.storage(directory)
|
|
41
|
+
const binary = await prepareNotification(directory, this.spawn)
|
|
42
|
+
return this.startRecorder(device, app, directory, duration, binary, own)
|
|
43
|
+
}
|
|
44
|
+
private async startRecorder(device: string, app: IosApp, directory: string, duration: number, binary: string, own: (recording: IosRecording) => void) {
|
|
45
|
+
const name = 'tools.mobiledebug.trace.' + randomUUID()
|
|
46
|
+
const listener = await listenNotification(binary, name, this.spawn)
|
|
47
|
+
let recording: IosRecording | undefined
|
|
48
|
+
try {
|
|
49
|
+
const current = await this.resolve(device, app.app_id)
|
|
50
|
+
if (current.pid !== app.pid || current.executable !== app.executable || current.started_at !== app.started_at) throw new Error('APP_IDENTITY_CHANGED')
|
|
51
|
+
const file = path.join(directory, 'capture.trace')
|
|
52
|
+
const recorder = this.spawn(getXcrunCmd(), ['xctrace', 'record', '--template', 'Time Profiler', '--device', device,
|
|
53
|
+
'--attach', String(app.pid), '--time-limit', `${duration}ms`, '--output', file, '--notify-tracing-started', name], duration + 65000)
|
|
54
|
+
recording = { recorder, file, ready_at_ms: 0 }; own(recording)
|
|
55
|
+
await Promise.race([listener.line('READY', 35000), recorder.done.then(result => { throw new Error(classifyTraceFailure(result)) })])
|
|
56
|
+
if (!recorder.alive()) throw new Error('RECORDER_EXITED')
|
|
57
|
+
recording.ready_at_ms = Date.now()
|
|
58
|
+
return recording
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (recording) await stopTraceChild(recording.recorder).catch(() => {})
|
|
61
|
+
throw error
|
|
62
|
+
} finally { await stopTraceChild(listener, 1000).catch(() => {}) }
|
|
63
|
+
}
|
|
64
|
+
stop(recording: IosRecording, timeout?: number) { return stopTraceChild(recording.recorder, timeout) }
|
|
65
|
+
async monitor(device: string, app: IosApp, recording: IosRecording) {
|
|
66
|
+
const current = await this.resolve(device, app.app_id)
|
|
67
|
+
if (current.pid !== app.pid || current.executable !== app.executable || current.started_at !== app.started_at) throw new Error('APP_IDENTITY_CHANGED')
|
|
68
|
+
try { if (await traceSize(recording.file) > IOS_TRACE_MAX_BYTES) throw new Error('STORAGE_LIMIT') }
|
|
69
|
+
catch (error) { if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) throw error }
|
|
70
|
+
}
|
|
71
|
+
validate(recording: IosRecording, device: string, app: IosApp) { return validateArtifact(recording.file, device, app, this.spawn) }
|
|
72
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { getXcrunCmd } from '../../utils/ios/utils.js'
|
|
4
|
+
import { traceCommand, spawnTrace, stopTraceChild, type TraceSpawn, type OwnedTraceChild } from './process.js'
|
|
5
|
+
|
|
6
|
+
// A private per-session listener avoids requiring a third-party notification utility.
|
|
7
|
+
const source = `#include <notify.h>
|
|
8
|
+
#include <dispatch/dispatch.h>
|
|
9
|
+
#include <stdio.h>
|
|
10
|
+
#include <stdlib.h>
|
|
11
|
+
int main(int argc, char **argv) {
|
|
12
|
+
if (argc != 2) return 2;
|
|
13
|
+
int token;
|
|
14
|
+
if (notify_register_dispatch(argv[1], &token, dispatch_get_main_queue(), ^(int t) {
|
|
15
|
+
puts("READY"); fflush(stdout); notify_cancel(t); exit(0);
|
|
16
|
+
}) != NOTIFY_STATUS_OK) return 3;
|
|
17
|
+
puts("ARMED"); fflush(stdout); dispatch_main();
|
|
18
|
+
}`
|
|
19
|
+
export async function prepareNotification(directory: string, spawn: TraceSpawn = spawnTrace) {
|
|
20
|
+
const file = path.join(directory, 'notification.c'), binary = path.join(directory, 'notification')
|
|
21
|
+
await fs.writeFile(file, source, { mode: 0o600 })
|
|
22
|
+
await traceCommand(getXcrunCmd(), ['clang', '-fblocks', file, '-o', binary], 30000, spawn)
|
|
23
|
+
await fs.chmod(binary, 0o700)
|
|
24
|
+
return binary
|
|
25
|
+
}
|
|
26
|
+
export async function listenNotification(binary: string, name: string, spawn: TraceSpawn = spawnTrace): Promise<OwnedTraceChild> {
|
|
27
|
+
const listener = spawn(binary, [name], 40000)
|
|
28
|
+
try { await listener.line('ARMED', 5000); return listener }
|
|
29
|
+
catch (error) { await stopTraceChild(listener, 1000).catch(() => {}); throw error }
|
|
30
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
export interface TraceCommandResult { code: number | null; output: string; timedOut: boolean }
|
|
4
|
+
export interface OwnedTraceChild {
|
|
5
|
+
done: Promise<TraceCommandResult>
|
|
6
|
+
alive(): boolean
|
|
7
|
+
signal(signal: NodeJS.Signals): void
|
|
8
|
+
line(text: string, timeout: number): Promise<void>
|
|
9
|
+
}
|
|
10
|
+
export type TraceSpawn = (command: string, args: string[], timeout: number) => OwnedTraceChild
|
|
11
|
+
const MAX_OUTPUT = 64 * 1024
|
|
12
|
+
|
|
13
|
+
export const spawnTrace: TraceSpawn = (command, args, timeout) => {
|
|
14
|
+
const child: ChildProcess = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] })
|
|
15
|
+
let output = '', settled = false, timedOut = false
|
|
16
|
+
const waiters = new Set<() => void>()
|
|
17
|
+
let force: ReturnType<typeof setTimeout> | undefined
|
|
18
|
+
const terminate = () => {
|
|
19
|
+
if (settled) return
|
|
20
|
+
child.kill('SIGTERM')
|
|
21
|
+
force ??= setTimeout(() => { if (!settled) child.kill('SIGKILL') }, 1000)
|
|
22
|
+
force.unref()
|
|
23
|
+
}
|
|
24
|
+
const timer = setTimeout(() => { timedOut = true; terminate() }, timeout)
|
|
25
|
+
timer.unref()
|
|
26
|
+
const done = new Promise<TraceCommandResult>(resolve => {
|
|
27
|
+
const finish = (code: number | null) => {
|
|
28
|
+
if (settled) return
|
|
29
|
+
settled = true; clearTimeout(timer); if (force) clearTimeout(force)
|
|
30
|
+
for (const wake of waiters) wake()
|
|
31
|
+
resolve({ code, output, timedOut })
|
|
32
|
+
}
|
|
33
|
+
child.on('error', () => finish(null))
|
|
34
|
+
child.on('close', finish)
|
|
35
|
+
const data = (chunk: Buffer) => {
|
|
36
|
+
output = (output + chunk.toString()).slice(-MAX_OUTPUT)
|
|
37
|
+
for (const wake of waiters) wake()
|
|
38
|
+
}
|
|
39
|
+
child.stdout?.on('data', data); child.stderr?.on('data', data)
|
|
40
|
+
})
|
|
41
|
+
return {
|
|
42
|
+
done, alive: () => !settled && child.exitCode === null && child.signalCode === null,
|
|
43
|
+
signal: signal => { if (!settled) child.kill(signal) },
|
|
44
|
+
line: (text, ms) => new Promise<void>((resolve, reject) => {
|
|
45
|
+
const deadline = setTimeout(() => { waiters.delete(check); reject(new Error('READINESS_TIMEOUT')) }, ms)
|
|
46
|
+
const check = () => {
|
|
47
|
+
if (output.split(/\r?\n/).includes(text)) { clearTimeout(deadline); waiters.delete(check); resolve() }
|
|
48
|
+
else if (settled) { clearTimeout(deadline); waiters.delete(check); reject(new Error('RECORDER_EXITED')) }
|
|
49
|
+
}
|
|
50
|
+
waiters.add(check); check()
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function traceCommand(command: string, args: string[], timeout = 10000, spawn: TraceSpawn = spawnTrace) {
|
|
56
|
+
const result = await spawn(command, args, timeout).done
|
|
57
|
+
if (result.code !== 0 || result.timedOut) throw new Error(result.timedOut ? 'ACTION_TIMEOUT' : 'COMMAND_FAILED')
|
|
58
|
+
return result.output.trim()
|
|
59
|
+
}
|
|
60
|
+
export async function stopTraceChild(child: OwnedTraceChild, timeout = 30000) {
|
|
61
|
+
if (child.alive()) child.signal('SIGINT')
|
|
62
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
63
|
+
const result = await Promise.race([
|
|
64
|
+
child.done,
|
|
65
|
+
new Promise<null>(resolve => { timer = setTimeout(() => resolve(null), timeout) })
|
|
66
|
+
])
|
|
67
|
+
if (timer) clearTimeout(timer)
|
|
68
|
+
if (result) return result
|
|
69
|
+
child.signal('SIGKILL')
|
|
70
|
+
let forceTimer: ReturnType<typeof setTimeout> | undefined
|
|
71
|
+
await Promise.race([child.done, new Promise<void>(resolve => { forceTimer = setTimeout(resolve, 1000) })])
|
|
72
|
+
if (forceTimer) clearTimeout(forceTimer)
|
|
73
|
+
throw new Error('ACTION_TIMEOUT')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function classifyTraceFailure(result: TraceCommandResult): string {
|
|
77
|
+
if (result.timedOut) return 'ACTION_TIMEOUT'
|
|
78
|
+
if (/permission|not permitted|authoriz|privacy|access denied/i.test(result.output)) return 'PROFILING_PERMISSION_REQUIRED'
|
|
79
|
+
if (/cannot find process|process.*not.*found/i.test(result.output)) return 'APP_NOT_RUNNING'
|
|
80
|
+
if (/disconnect|device.*unavailable/i.test(result.output)) return 'TARGET_DISCONNECTED'
|
|
81
|
+
return 'RECORDER_EXITED'
|
|
82
|
+
}
|