mobster-cli 0.4.3__py3-none-any.whl
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.
- mobile_agent/__init__.py +4 -0
- mobile_agent/__main__.py +598 -0
- mobile_agent/accounts/__init__.py +1 -0
- mobile_agent/accounts/context.py +80 -0
- mobile_agent/accounts/mcp.py +55 -0
- mobile_agent/accounts/pacing.py +318 -0
- mobile_agent/accounts/register.py +78 -0
- mobile_agent/accounts/routes.py +212 -0
- mobile_agent/accounts/routing.py +69 -0
- mobile_agent/accounts/screen.py +273 -0
- mobile_agent/accounts/stats.py +54 -0
- mobile_agent/accounts/store.py +394 -0
- mobile_agent/agent_hooks.py +123 -0
- mobile_agent/ai_plans/__init__.py +7 -0
- mobile_agent/ai_plans/agents.py +96 -0
- mobile_agent/ai_plans/claude_code.py +208 -0
- mobile_agent/ai_plans/cli.py +400 -0
- mobile_agent/ai_plans/codex.py +258 -0
- mobile_agent/ai_plans/compat_chat.py +307 -0
- mobile_agent/ai_plans/device_login.py +164 -0
- mobile_agent/ai_plans/draft_client.py +68 -0
- mobile_agent/ai_plans/gemini.py +137 -0
- mobile_agent/ai_plans/instructions.py +65 -0
- mobile_agent/ai_plans/mcp_http.py +160 -0
- mobile_agent/ai_plans/phone_tools.py +892 -0
- mobile_agent/ai_plans/providers.py +235 -0
- mobile_agent/ai_plans/routes.py +160 -0
- mobile_agent/ai_plans/runner.py +400 -0
- mobile_agent/ai_plans/vault.py +114 -0
- mobile_agent/alerts.py +337 -0
- mobile_agent/android/__init__.py +18 -0
- mobile_agent/android/adb.py +268 -0
- mobile_agent/android/conditions.py +128 -0
- mobile_agent/android/doctor.py +54 -0
- mobile_agent/android/driver.py +137 -0
- mobile_agent/android/emulator.py +216 -0
- mobile_agent/android/logs.py +85 -0
- mobile_agent/android/manager.py +225 -0
- mobile_agent/android/records.py +31 -0
- mobile_agent/android/tree.py +96 -0
- mobile_agent/answer_shapes.py +230 -0
- mobile_agent/api_devices.py +333 -0
- mobile_agent/api_errors.py +17 -0
- mobile_agent/api_get.py +249 -0
- mobile_agent/api_http.py +467 -0
- mobile_agent/api_post.py +453 -0
- mobile_agent/api_routes.py +173 -0
- mobile_agent/app_api/__init__.py +4 -0
- mobile_agent/app_api/cli.py +151 -0
- mobile_agent/app_api/endpoints.py +193 -0
- mobile_agent/app_api/mcp.py +54 -0
- mobile_agent/app_api/openapi.py +146 -0
- mobile_agent/app_api/reading.py +172 -0
- mobile_agent/app_api/register.py +10 -0
- mobile_agent/app_api/screen.py +216 -0
- mobile_agent/app_api/service.py +382 -0
- mobile_agent/app_api/sessions.py +404 -0
- mobile_agent/app_api/tokens.py +167 -0
- mobile_agent/app_choice.py +131 -0
- mobile_agent/app_icons.py +183 -0
- mobile_agent/app_killer.py +69 -0
- mobile_agent/artifacts.py +606 -0
- mobile_agent/attach/__init__.py +13 -0
- mobile_agent/attach/cli.py +46 -0
- mobile_agent/attach/flutter.py +28 -0
- mobile_agent/attach/score.py +119 -0
- mobile_agent/attach/web.py +13 -0
- mobile_agent/attachments/__init__.py +1 -0
- mobile_agent/attachments/agent.py +418 -0
- mobile_agent/attachments/extract.py +217 -0
- mobile_agent/attachments/mcp.py +104 -0
- mobile_agent/attachments/register.py +73 -0
- mobile_agent/attachments/routes.py +326 -0
- mobile_agent/attachments/service.py +170 -0
- mobile_agent/attachments/sniff.py +152 -0
- mobile_agent/attachments/store.py +441 -0
- mobile_agent/audit_verify.py +436 -0
- mobile_agent/bench/README.md +410 -0
- mobile_agent/bench/__init__.py +0 -0
- mobile_agent/bench/__main__.py +394 -0
- mobile_agent/bench/ab.py +168 -0
- mobile_agent/bench/actuation.py +368 -0
- mobile_agent/bench/agents/__init__.py +9 -0
- mobile_agent/bench/agents/base.py +105 -0
- mobile_agent/bench/agents/plan_clients.py +245 -0
- mobile_agent/bench/agents/smart.py +533 -0
- mobile_agent/bench/checks.py +358 -0
- mobile_agent/bench/decide_gate.py +214 -0
- mobile_agent/bench/diagnose.py +181 -0
- mobile_agent/bench/evals/__init__.py +14 -0
- mobile_agent/bench/evals/build.py +565 -0
- mobile_agent/bench/evals/checkpoint.py +214 -0
- mobile_agent/bench/evals/irt.py +219 -0
- mobile_agent/bench/evals/l0.py +647 -0
- mobile_agent/bench/evals/l1.py +866 -0
- mobile_agent/bench/evals/models.py +124 -0
- mobile_agent/bench/evals/pairwise.py +189 -0
- mobile_agent/bench/evals/proxy.py +90 -0
- mobile_agent/bench/evals/sets.py +149 -0
- mobile_agent/bench/evals/traces.py +446 -0
- mobile_agent/bench/fixtures.py +176 -0
- mobile_agent/bench/guard_replay.py +129 -0
- mobile_agent/bench/h2-manifest.json +20 -0
- mobile_agent/bench/h3-manifest.json +20 -0
- mobile_agent/bench/heldout-manifest.json +32 -0
- mobile_agent/bench/heldout.py +375 -0
- mobile_agent/bench/iosworld.py +901 -0
- mobile_agent/bench/iosworld_stats.py +510 -0
- mobile_agent/bench/llm_units.py +116 -0
- mobile_agent/bench/manifest.json +93 -0
- mobile_agent/bench/metrics.py +66 -0
- mobile_agent/bench/phone.py +44 -0
- mobile_agent/bench/replay.py +172 -0
- mobile_agent/bench/report.py +207 -0
- mobile_agent/bench/runner.py +456 -0
- mobile_agent/bench/safety.py +310 -0
- mobile_agent/bench/stream_timing.py +213 -0
- mobile_agent/bench/suite.py +444 -0
- mobile_agent/bench/sweep.py +176 -0
- mobile_agent/bench/tape.py +798 -0
- mobile_agent/bench/tape_replay.py +88 -0
- mobile_agent/bench/trace_evals.py +128 -0
- mobile_agent/bench/trace_view.py +81 -0
- mobile_agent/bench/truth/iphone15pro.json +138 -0
- mobile_agent/bench/truth.py +340 -0
- mobile_agent/bench/tuning.py +543 -0
- mobile_agent/bench/tuning_analysis.py +656 -0
- mobile_agent/bench/vague-heldout-manifest.json +12 -0
- mobile_agent/bench/vague_rubric.py +135 -0
- mobile_agent/bench/vague_run.py +243 -0
- mobile_agent/brand.py +454 -0
- mobile_agent/brand_fonts.py +756 -0
- mobile_agent/build_identity.py +21 -0
- mobile_agent/cable_bt.py +623 -0
- mobile_agent/cable_bt_runtime.py +128 -0
- mobile_agent/catalog.py +69 -0
- mobile_agent/chart_svg.py +415 -0
- mobile_agent/chat_here.py +189 -0
- mobile_agent/cli_constants.py +5 -0
- mobile_agent/cli_parser.py +611 -0
- mobile_agent/cli_run.py +115 -0
- mobile_agent/compiled_read/__init__.py +21 -0
- mobile_agent/compiled_read/delta.py +89 -0
- mobile_agent/compiled_read/extractors.py +202 -0
- mobile_agent/compiled_read/routes.py +56 -0
- mobile_agent/completion.py +267 -0
- mobile_agent/compose.py +48 -0
- mobile_agent/config.py +114 -0
- mobile_agent/connectors/__init__.py +1 -0
- mobile_agent/connectors/api_session.py +76 -0
- mobile_agent/connectors/callback_page.py +146 -0
- mobile_agent/connectors/client.py +582 -0
- mobile_agent/connectors/github_app.py +55 -0
- mobile_agent/connectors/graph_api.py +396 -0
- mobile_agent/connectors/imports.py +170 -0
- mobile_agent/connectors/issue.py +66 -0
- mobile_agent/connectors/keystore.py +85 -0
- mobile_agent/connectors/marks.py +222 -0
- mobile_agent/connectors/oauth.py +476 -0
- mobile_agent/connectors/picker.py +228 -0
- mobile_agent/connectors/presets.py +591 -0
- mobile_agent/connectors/register.py +48 -0
- mobile_agent/connectors/routes.py +153 -0
- mobile_agent/connectors/service.py +966 -0
- mobile_agent/connectors/shared.py +136 -0
- mobile_agent/connectors/skill.py +446 -0
- mobile_agent/connectors/slack_api.py +269 -0
- mobile_agent/connectors/steps.py +92 -0
- mobile_agent/connectors/store.py +144 -0
- mobile_agent/connectors/vault.py +72 -0
- mobile_agent/console.py +178 -0
- mobile_agent/content_queue/__init__.py +1 -0
- mobile_agent/content_queue/client.py +263 -0
- mobile_agent/content_queue/importing.py +153 -0
- mobile_agent/content_queue/media_check.py +224 -0
- mobile_agent/content_queue/prompt.py +64 -0
- mobile_agent/content_queue/register.py +91 -0
- mobile_agent/content_queue/routes.py +370 -0
- mobile_agent/content_queue/runner.py +475 -0
- mobile_agent/content_queue/store.py +521 -0
- mobile_agent/contract.py +996 -0
- mobile_agent/contract_asks.py +248 -0
- mobile_agent/contract_prompt.py +45 -0
- mobile_agent/contract_screen.py +981 -0
- mobile_agent/contract_view.py +84 -0
- mobile_agent/corpus/__init__.py +26 -0
- mobile_agent/corpus/cli.py +63 -0
- mobile_agent/corpus/consent.py +55 -0
- mobile_agent/corpus/listener.py +76 -0
- mobile_agent/corpus/register.py +9 -0
- mobile_agent/corpus/skeleton.py +72 -0
- mobile_agent/corpus/store.py +143 -0
- mobile_agent/costs.py +161 -0
- mobile_agent/decider_scope.py +40 -0
- mobile_agent/decision_provider.py +189 -0
- mobile_agent/deeplinks.py +352 -0
- mobile_agent/device_info.py +116 -0
- mobile_agent/device_manager.py +978 -0
- mobile_agent/device_status.py +195 -0
- mobile_agent/device_targets.py +217 -0
- mobile_agent/devices.py +676 -0
- mobile_agent/devkit/__init__.py +64 -0
- mobile_agent/devkit/account_cli.py +90 -0
- mobile_agent/devkit/asc.py +562 -0
- mobile_agent/devkit/asc_cli.py +214 -0
- mobile_agent/devkit/ci.py +737 -0
- mobile_agent/devkit/ci_cli.py +196 -0
- mobile_agent/devkit/ci_hosts.py +194 -0
- mobile_agent/devkit/es256.py +197 -0
- mobile_agent/devkit/expo.py +440 -0
- mobile_agent/devkit/expo_cli.py +405 -0
- mobile_agent/devkit/expo_dev.py +203 -0
- mobile_agent/devkit/expo_routes.py +90 -0
- mobile_agent/devkit/fixtures_cli.py +133 -0
- mobile_agent/devkit/fixtures_routes.py +82 -0
- mobile_agent/devkit/links.py +375 -0
- mobile_agent/devkit/links_cli.py +123 -0
- mobile_agent/devkit/links_routes.py +83 -0
- mobile_agent/devkit/nightly.py +152 -0
- mobile_agent/devkit/shots.py +203 -0
- mobile_agent/devkit/testflight.py +155 -0
- mobile_agent/devkit/testflight_cli.py +127 -0
- mobile_agent/devkit/testflight_routes.py +111 -0
- mobile_agent/devkit/xcode.py +236 -0
- mobile_agent/devkit/xcode_cli.py +101 -0
- mobile_agent/devtools.py +419 -0
- mobile_agent/doctor.py +509 -0
- mobile_agent/doctor_fix.py +467 -0
- mobile_agent/downloads.py +55 -0
- mobile_agent/drivers.py +793 -0
- mobile_agent/dry_run.py +102 -0
- mobile_agent/effect_ledger.py +83 -0
- mobile_agent/engine_health.py +378 -0
- mobile_agent/engine_hooks.py +96 -0
- mobile_agent/engines.py +408 -0
- mobile_agent/env_file.py +63 -0
- mobile_agent/env_secrets.py +149 -0
- mobile_agent/errors.py +15 -0
- mobile_agent/evals/__init__.py +3 -0
- mobile_agent/evals/app_survey.py +149 -0
- mobile_agent/evals/development/__init__.py +1 -0
- mobile_agent/evals/development/score.py +276 -0
- mobile_agent/evals/latency_report.py +312 -0
- mobile_agent/evals/offline_report.py +374 -0
- mobile_agent/evals/oracles.py +298 -0
- mobile_agent/evals/redteam/__init__.py +15 -0
- mobile_agent/evals/redteam/__main__.py +72 -0
- mobile_agent/evals/redteam/cases.py +730 -0
- mobile_agent/evals/redteam/harness.py +604 -0
- mobile_agent/evals/redteam/live.py +139 -0
- mobile_agent/evals/redteam/plain.py +85 -0
- mobile_agent/evals/vision/__init__.py +0 -0
- mobile_agent/evals/vision/images/dog_001.jpg +0 -0
- mobile_agent/evals/vision/images/dog_002.jpg +0 -0
- mobile_agent/evals/vision/images/dog_003.jpg +0 -0
- mobile_agent/evals/vision/images/dog_004.jpg +0 -0
- mobile_agent/evals/vision/images/dog_005.jpg +0 -0
- mobile_agent/evals/vision/images/dog_006.jpg +0 -0
- mobile_agent/evals/vision/images/dog_007.jpg +0 -0
- mobile_agent/evals/vision/images/dog_010.jpg +0 -0
- mobile_agent/evals/vision/images/dog_011.jpg +0 -0
- mobile_agent/evals/vision/images/dog_013.jpg +0 -0
- mobile_agent/evals/vision/images/dog_014.jpg +0 -0
- mobile_agent/evals/vision/images/dog_015.jpg +0 -0
- mobile_agent/evals/vision/images/dog_016.jpg +0 -0
- mobile_agent/evals/vision/images/dog_017.jpg +0 -0
- mobile_agent/evals/vision/images/dog_019.jpg +0 -0
- mobile_agent/evals/vision/images/dog_020.jpg +0 -0
- mobile_agent/evals/vision/images/dog_021.jpg +0 -0
- mobile_agent/evals/vision/images/dog_022.jpg +0 -0
- mobile_agent/evals/vision/images/dog_023.jpg +0 -0
- mobile_agent/evals/vision/images/dog_024.jpg +0 -0
- mobile_agent/evals/vision/images/dog_026.jpg +0 -0
- mobile_agent/evals/vision/images/dog_027.jpg +0 -0
- mobile_agent/evals/vision/images/dog_034.jpg +0 -0
- mobile_agent/evals/vision/images/dog_035.jpg +0 -0
- mobile_agent/evals/vision/images/dog_038.jpg +0 -0
- mobile_agent/evals/vision/images/dog_042.jpg +0 -0
- mobile_agent/evals/vision/images/dog_047.jpg +0 -0
- mobile_agent/evals/vision/images/dog_052.jpg +0 -0
- mobile_agent/evals/vision/images/dog_058.jpg +0 -0
- mobile_agent/evals/vision/images/dog_062.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_000.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_010.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_011.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_015.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_016.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_017.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_019.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_021.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_025.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_026.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_027.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_030.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_032.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_033.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_034.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_035.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_036.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_037.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_039.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_040.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_041.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_042.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_043.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_044.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_046.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_047.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_048.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_049.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_051.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_053.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_054.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_055.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_058.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_002.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_003.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_004.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_007.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_009.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_010.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_011.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_012.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_013.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_014.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_016.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_017.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_023.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_027.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_029.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_032.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_033.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_036.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_041.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_043.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_047.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_049.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_051.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_053.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_056.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_057.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_058.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_060.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_061.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_062.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_063.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_064.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_065.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_066.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_067.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_068.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_069.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_070.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_071.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_000.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_001.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_002.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_004.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_005.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_008.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_009.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_010.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_012.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_013.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_015.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_016.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_017.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_018.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_021.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_022.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_023.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_024.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_030.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_031.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_032.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_033.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_034.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_036.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_038.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_039.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_040.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_041.jpg +0 -0
- mobile_agent/evals/vision/manifest.json +2355 -0
- mobile_agent/eventbus.py +110 -0
- mobile_agent/explain.py +34 -0
- mobile_agent/failure_line.py +197 -0
- mobile_agent/fanout.py +663 -0
- mobile_agent/feedback/__init__.py +2 -0
- mobile_agent/feedback/bundle.py +204 -0
- mobile_agent/feedback/outbox.py +196 -0
- mobile_agent/feedback/register.py +9 -0
- mobile_agent/feedback/routes.py +177 -0
- mobile_agent/fleet.py +841 -0
- mobile_agent/fleet_health.py +729 -0
- mobile_agent/fleet_phone.py +186 -0
- mobile_agent/frontier/__init__.py +79 -0
- mobile_agent/frontier/act.py +380 -0
- mobile_agent/frontier/actions.py +70 -0
- mobile_agent/frontier/agent.py +256 -0
- mobile_agent/frontier/approvals.py +366 -0
- mobile_agent/frontier/hooks.py +199 -0
- mobile_agent/frontier/interrupts.py +125 -0
- mobile_agent/frontier/observe.py +331 -0
- mobile_agent/frontier/phases.py +381 -0
- mobile_agent/frontier/pipeline.py +61 -0
- mobile_agent/frontier/prompt.py +384 -0
- mobile_agent/frontier/protocol.py +91 -0
- mobile_agent/frontier/safety.py +467 -0
- mobile_agent/frontier/saved.py +375 -0
- mobile_agent/frontier/screen.py +419 -0
- mobile_agent/frontier/skills.py +147 -0
- mobile_agent/frontier/stages/__init__.py +1 -0
- mobile_agent/frontier/stages/after.py +192 -0
- mobile_agent/frontier/stages/approve.py +138 -0
- mobile_agent/frontier/stages/controls.py +115 -0
- mobile_agent/frontier/stages/ends.py +102 -0
- mobile_agent/frontier/stages/gate.py +92 -0
- mobile_agent/frontier/stages/guards.py +132 -0
- mobile_agent/frontier/stages/points.py +155 -0
- mobile_agent/frontier/stages/send.py +177 -0
- mobile_agent/frontier/turn.py +151 -0
- mobile_agent/full_disk_access.py +83 -0
- mobile_agent/gemini.py +69 -0
- mobile_agent/genuine.py +120 -0
- mobile_agent/graph/__init__.py +4 -0
- mobile_agent/graph/catalog.py +789 -0
- mobile_agent/graph/chain.py +306 -0
- mobile_agent/graph/dispatch.py +98 -0
- mobile_agent/graph/draft.py +373 -0
- mobile_agent/graph/edit.py +76 -0
- mobile_agent/graph/estimate.py +189 -0
- mobile_agent/graph/expr.py +562 -0
- mobile_agent/graph/functions.py +271 -0
- mobile_agent/graph/infer.py +144 -0
- mobile_agent/graph/nodes.py +936 -0
- mobile_agent/graph/phone_steps.py +178 -0
- mobile_agent/graph/rerun.py +327 -0
- mobile_agent/graph/run.py +899 -0
- mobile_agent/graph/schema.py +842 -0
- mobile_agent/graph/state.py +161 -0
- mobile_agent/graph/values.py +198 -0
- mobile_agent/harness/__init__.py +2 -0
- mobile_agent/harness/checkpoints.py +468 -0
- mobile_agent/harness/control.py +101 -0
- mobile_agent/harness/prewarm.py +213 -0
- mobile_agent/harness/register.py +43 -0
- mobile_agent/harness/routes.py +114 -0
- mobile_agent/harness/settings.py +66 -0
- mobile_agent/harness/tools/__init__.py +67 -0
- mobile_agent/harness/tools/ask_user.py +102 -0
- mobile_agent/harness/tools/watch.py +120 -0
- mobile_agent/harness_api.py +815 -0
- mobile_agent/hedge.py +203 -0
- mobile_agent/help_topics.py +305 -0
- mobile_agent/hints.py +248 -0
- mobile_agent/history.py +69 -0
- mobile_agent/home.py +214 -0
- mobile_agent/http_pool.py +175 -0
- mobile_agent/image_policy.py +139 -0
- mobile_agent/inbox.py +148 -0
- mobile_agent/installed_apps.py +344 -0
- mobile_agent/integrations/__init__.py +1 -0
- mobile_agent/integrations/cli.py +472 -0
- mobile_agent/integrations/client_files.py +65 -0
- mobile_agent/integrations/clients.py +998 -0
- mobile_agent/integrations/doctor.py +185 -0
- mobile_agent/integrations/jsonc.py +383 -0
- mobile_agent/integrations/routes.py +135 -0
- mobile_agent/integrations/rules.py +112 -0
- mobile_agent/integrations/server_options.py +46 -0
- mobile_agent/integrations/skill.py +152 -0
- mobile_agent/integrations/skill_files.py +10 -0
- mobile_agent/integrations/textedit.py +309 -0
- mobile_agent/intents/__init__.py +17 -0
- mobile_agent/intents/atlas.py +216 -0
- mobile_agent/intents/callback.py +44 -0
- mobile_agent/intents/cli.py +55 -0
- mobile_agent/intents/harvest.py +91 -0
- mobile_agent/intents/plan.py +93 -0
- mobile_agent/ios/__init__.py +1 -0
- mobile_agent/ios/guide.py +16 -0
- mobile_agent/ios/kb.py +212 -0
- mobile_agent/ios/kb_data.py +1588 -0
- mobile_agent/ios/mcp.py +44 -0
- mobile_agent/ios/register.py +8 -0
- mobile_agent/ios/system.py +369 -0
- mobile_agent/journal.py +531 -0
- mobile_agent/keychain_acl.py +374 -0
- mobile_agent/keys.py +395 -0
- mobile_agent/latency_overlap.py +36 -0
- mobile_agent/latency_trace.py +170 -0
- mobile_agent/learned/__init__.py +15 -0
- mobile_agent/learned/app_path.py +160 -0
- mobile_agent/learned/code_read.py +38 -0
- mobile_agent/learned/parts.py +546 -0
- mobile_agent/learned/programs.py +691 -0
- mobile_agent/learned/register.py +171 -0
- mobile_agent/learned/routes.py +61 -0
- mobile_agent/learned/taught.py +329 -0
- mobile_agent/learned/teach.py +538 -0
- mobile_agent/learned/workflows.py +647 -0
- mobile_agent/learned/zero.py +41 -0
- mobile_agent/links.py +37 -0
- mobile_agent/listen.py +607 -0
- mobile_agent/lock_spike.py +278 -0
- mobile_agent/lockscreen.py +526 -0
- mobile_agent/login.py +354 -0
- mobile_agent/longform/__init__.py +15 -0
- mobile_agent/longform/ads.py +191 -0
- mobile_agent/longform/blocks.py +117 -0
- mobile_agent/longform/budget.py +265 -0
- mobile_agent/longform/context.py +285 -0
- mobile_agent/longform/costs.py +93 -0
- mobile_agent/longform/digest.py +145 -0
- mobile_agent/longform/extract.py +508 -0
- mobile_agent/longform/feed.py +505 -0
- mobile_agent/longform/followup.py +168 -0
- mobile_agent/longform/frames.py +82 -0
- mobile_agent/longform/identity.py +87 -0
- mobile_agent/longform/inbox.py +221 -0
- mobile_agent/longform/items.py +425 -0
- mobile_agent/longform/models.py +115 -0
- mobile_agent/longform/notebook.py +329 -0
- mobile_agent/longform/plan.py +614 -0
- mobile_agent/longform/recall.py +195 -0
- mobile_agent/longform/receipts.py +81 -0
- mobile_agent/longform/register.py +124 -0
- mobile_agent/longform/report.py +343 -0
- mobile_agent/longform/routes.py +102 -0
- mobile_agent/longform/session.py +644 -0
- mobile_agent/longform/store.py +146 -0
- mobile_agent/longform/synthesis.py +347 -0
- mobile_agent/mac_tools.py +223 -0
- mobile_agent/manual_control.py +143 -0
- mobile_agent/maps.py +149 -0
- mobile_agent/marks.py +92 -0
- mobile_agent/mcp_server/__init__.py +3 -0
- mobile_agent/mcp_server/__main__.py +29 -0
- mobile_agent/mcp_server/action_journal.py +24 -0
- mobile_agent/mcp_server/action_tools.py +638 -0
- mobile_agent/mcp_server/approvals.py +219 -0
- mobile_agent/mcp_server/catalog.py +92 -0
- mobile_agent/mcp_server/cli.py +194 -0
- mobile_agent/mcp_server/codes.py +147 -0
- mobile_agent/mcp_server/device_tools.py +306 -0
- mobile_agent/mcp_server/direct.py +272 -0
- mobile_agent/mcp_server/expo_tools.py +105 -0
- mobile_agent/mcp_server/phone_tools.py +510 -0
- mobile_agent/mcp_server/protocol.py +364 -0
- mobile_agent/mcp_server/registry.py +55 -0
- mobile_agent/mcp_server/run_tools.py +531 -0
- mobile_agent/mcp_server/schemas.py +425 -0
- mobile_agent/mcp_server/screen_tools.py +168 -0
- mobile_agent/mcp_server/session.py +469 -0
- mobile_agent/mcp_server/support.py +145 -0
- mobile_agent/mcp_server/tools.py +493 -0
- mobile_agent/memory/__init__.py +9 -0
- mobile_agent/memory/cli.py +207 -0
- mobile_agent/memory/extract.py +191 -0
- mobile_agent/memory/listener.py +100 -0
- mobile_agent/memory/model.py +144 -0
- mobile_agent/memory/project.py +96 -0
- mobile_agent/memory/provider.py +127 -0
- mobile_agent/memory/register.py +21 -0
- mobile_agent/memory/retrieval.py +172 -0
- mobile_agent/memory/routes.py +216 -0
- mobile_agent/memory/runs.py +124 -0
- mobile_agent/memory/store.py +806 -0
- mobile_agent/memory/writer.py +278 -0
- mobile_agent/mirroring.py +585 -0
- mobile_agent/mirroring_runtime.py +221 -0
- mobile_agent/model_clients.py +604 -0
- mobile_agent/narrate.py +813 -0
- mobile_agent/native_helpers.py +84 -0
- mobile_agent/netxray/__init__.py +1 -0
- mobile_agent/netxray/analyze.py +322 -0
- mobile_agent/netxray/attribution.py +62 -0
- mobile_agent/netxray/capture.py +88 -0
- mobile_agent/netxray/cli.py +142 -0
- mobile_agent/netxray/job.py +219 -0
- mobile_agent/netxray/mobster-pcap.c +560 -0
- mobile_agent/netxray/packets.py +410 -0
- mobile_agent/netxray/pcapng.py +223 -0
- mobile_agent/netxray/register.py +93 -0
- mobile_agent/netxray/report.py +214 -0
- mobile_agent/netxray/store.py +48 -0
- mobile_agent/netxray/summary.py +157 -0
- mobile_agent/netxray/target.py +67 -0
- mobile_agent/netxray/tool.py +66 -0
- mobile_agent/netxray/trackers.py +194 -0
- mobile_agent/notify_triggers/__init__.py +1 -0
- mobile_agent/notify_triggers/rules.py +39 -0
- mobile_agent/notify_triggers/sources.py +315 -0
- mobile_agent/notify_triggers/watch.py +135 -0
- mobile_agent/offers.py +86 -0
- mobile_agent/orchestrate.py +215 -0
- mobile_agent/outlets/__init__.py +1 -0
- mobile_agent/outlets/connect.py +249 -0
- mobile_agent/outlets/folder.py +180 -0
- mobile_agent/outlets/hooks.py +453 -0
- mobile_agent/outlets/inbound.py +472 -0
- mobile_agent/outlets/inbound_reply.py +57 -0
- mobile_agent/outlets/inbound_routes.py +81 -0
- mobile_agent/outlets/kinds.py +308 -0
- mobile_agent/outlets/mail.py +110 -0
- mobile_agent/outlets/net.py +86 -0
- mobile_agent/outlets/notion.py +244 -0
- mobile_agent/outlets/register.py +56 -0
- mobile_agent/outlets/routes.py +135 -0
- mobile_agent/outlets/service.py +580 -0
- mobile_agent/outlets/sheets.py +169 -0
- mobile_agent/outlets/slack.py +177 -0
- mobile_agent/outlets/store.py +194 -0
- mobile_agent/outlets/vault.py +80 -0
- mobile_agent/outlets/webhook.py +316 -0
- mobile_agent/output_contract.py +141 -0
- mobile_agent/owner_presence.py +211 -0
- mobile_agent/ownership.py +219 -0
- mobile_agent/packs/__init__.py +1 -0
- mobile_agent/packs/calendar.py +243 -0
- mobile_agent/packs/context.py +68 -0
- mobile_agent/packs/core.py +757 -0
- mobile_agent/packs/front_door.py +316 -0
- mobile_agent/packs/heal.py +214 -0
- mobile_agent/packs/health.py +122 -0
- mobile_agent/packs/insights.py +76 -0
- mobile_agent/packs/instagram.py +640 -0
- mobile_agent/packs/linkedin.py +106 -0
- mobile_agent/packs/maps.py +196 -0
- mobile_agent/packs/mcp.py +149 -0
- mobile_agent/packs/messages.py +270 -0
- mobile_agent/packs/notes.py +226 -0
- mobile_agent/packs/photos.py +166 -0
- mobile_agent/packs/pins.py +47 -0
- mobile_agent/packs/reddit.py +110 -0
- mobile_agent/packs/register.py +276 -0
- mobile_agent/packs/rows.py +108 -0
- mobile_agent/packs/safari.py +143 -0
- mobile_agent/packs/settings.py +253 -0
- mobile_agent/packs/social.py +300 -0
- mobile_agent/packs/threads.py +97 -0
- mobile_agent/packs/tiktok.py +455 -0
- mobile_agent/packs/tools.py +290 -0
- mobile_agent/packs/x.py +127 -0
- mobile_agent/packs/youtube.py +199 -0
- mobile_agent/parity.py +270 -0
- mobile_agent/paths.py +99 -0
- mobile_agent/perception.py +470 -0
- mobile_agent/permissions/__init__.py +32 -0
- mobile_agent/permissions/consequence.py +535 -0
- mobile_agent/permissions/exceptions.py +99 -0
- mobile_agent/permissions/gate.py +316 -0
- mobile_agent/permissions/outside.py +316 -0
- mobile_agent/permissions/policy.py +288 -0
- mobile_agent/permissions/project_policy.py +411 -0
- mobile_agent/permissions/register.py +148 -0
- mobile_agent/permissions/rules.py +291 -0
- mobile_agent/phone_io/__init__.py +146 -0
- mobile_agent/phone_io/cli.py +427 -0
- mobile_agent/phone_io/clipboard.py +78 -0
- mobile_agent/phone_io/controls.py +78 -0
- mobile_agent/phone_io/files.py +289 -0
- mobile_agent/phone_io/handoff.py +123 -0
- mobile_agent/phone_io/icloud.py +118 -0
- mobile_agent/phone_io/install.py +188 -0
- mobile_agent/phone_io/network_capture.py +188 -0
- mobile_agent/phone_io/network_cli.py +138 -0
- mobile_agent/phone_io/network_profiles.py +138 -0
- mobile_agent/phone_io/photos.py +512 -0
- mobile_agent/phone_io/recording.py +115 -0
- mobile_agent/phone_io/secrets.py +146 -0
- mobile_agent/phone_io/transfer.py +895 -0
- mobile_agent/phone_io/tunnel.py +129 -0
- mobile_agent/phone_ready.py +91 -0
- mobile_agent/pinning.py +158 -0
- mobile_agent/plan_gate.py +807 -0
- mobile_agent/plugins/__init__.py +3 -0
- mobile_agent/plugins/install.py +163 -0
- mobile_agent/plugins/manifest.py +242 -0
- mobile_agent/plugins/register.py +15 -0
- mobile_agent/plugins/routes.py +82 -0
- mobile_agent/plugins/service.py +322 -0
- mobile_agent/plugins/skills.py +76 -0
- mobile_agent/plugins/store.py +79 -0
- mobile_agent/pool.py +285 -0
- mobile_agent/power_watch.py +275 -0
- mobile_agent/premium_model.py +22 -0
- mobile_agent/project_env.py +291 -0
- mobile_agent/project_env_cli.py +74 -0
- mobile_agent/project_env_routes.py +55 -0
- mobile_agent/projects/__init__.py +27 -0
- mobile_agent/projects/ci.py +332 -0
- mobile_agent/projects/clone.py +339 -0
- mobile_agent/projects/drafts.py +158 -0
- mobile_agent/projects/folder.py +436 -0
- mobile_agent/projects/github.py +131 -0
- mobile_agent/projects/register.py +31 -0
- mobile_agent/projects/revisions.py +253 -0
- mobile_agent/projects/routes.py +234 -0
- mobile_agent/projects/runs.py +297 -0
- mobile_agent/projects/service.py +695 -0
- mobile_agent/projects/setup.py +84 -0
- mobile_agent/projects/store.py +379 -0
- mobile_agent/projects/sync.py +380 -0
- mobile_agent/projects/workflow_files.py +270 -0
- mobile_agent/replay_core/__init__.py +26 -0
- mobile_agent/replay_core/checks.py +594 -0
- mobile_agent/replay_core/engine.py +596 -0
- mobile_agent/replay_core/heal.py +364 -0
- mobile_agent/replay_core/path.py +240 -0
- mobile_agent/replay_core/recorder.py +206 -0
- mobile_agent/replay_core/store.py +171 -0
- mobile_agent/result_blocks.py +941 -0
- mobile_agent/run.py +642 -0
- mobile_agent/run_delivery.py +123 -0
- mobile_agent/run_executor.py +200 -0
- mobile_agent/run_export.py +284 -0
- mobile_agent/run_gif.py +891 -0
- mobile_agent/runner_hints.py +86 -0
- mobile_agent/runner_renewal.py +205 -0
- mobile_agent/runtime_create.py +549 -0
- mobile_agent/runtime_driver.py +458 -0
- mobile_agent/runtime_limits.py +319 -0
- mobile_agent/runtime_queue.py +394 -0
- mobile_agent/runtime_runs.py +383 -0
- mobile_agent/runtime_status.py +181 -0
- mobile_agent/runtime_teach.py +295 -0
- mobile_agent/runtime_work.py +599 -0
- mobile_agent/screen_text.py +101 -0
- mobile_agent/secret_filter.py +113 -0
- mobile_agent/serve.py +160 -0
- mobile_agent/server.py +363 -0
- mobile_agent/service_support.py +42 -0
- mobile_agent/settle/__init__.py +1 -0
- mobile_agent/settle/predict.py +102 -0
- mobile_agent/settle/replay.py +92 -0
- mobile_agent/settle/store.py +93 -0
- mobile_agent/settle/stream.py +43 -0
- mobile_agent/setup_cli.py +225 -0
- mobile_agent/setup_errors.py +87 -0
- mobile_agent/setup_service.py +527 -0
- mobile_agent/share.py +104 -0
- mobile_agent/signing.py +184 -0
- mobile_agent/sim/__init__.py +6 -0
- mobile_agent/sim/__main__.py +26 -0
- mobile_agent/sim/api.py +952 -0
- mobile_agent/sim/apps.py +172 -0
- mobile_agent/sim/biometrics.py +84 -0
- mobile_agent/sim/cli.py +271 -0
- mobile_agent/sim/doctor.py +293 -0
- mobile_agent/sim/governor.py +90 -0
- mobile_agent/sim/holders.py +88 -0
- mobile_agent/sim/models.py +62 -0
- mobile_agent/sim/registry.py +161 -0
- mobile_agent/sim/simctl.py +269 -0
- mobile_agent/sim/wda.py +618 -0
- mobile_agent/sim/xcode.py +74 -0
- mobile_agent/skills/__init__.py +12 -0
- mobile_agent/skills/codes.py +597 -0
- mobile_agent/skills/notifications.py +281 -0
- mobile_agent/skills/open_url.py +127 -0
- mobile_agent/smart_events.py +588 -0
- mobile_agent/smart_result.py +490 -0
- mobile_agent/smart_run.py +413 -0
- mobile_agent/state.py +662 -0
- mobile_agent/states.py +217 -0
- mobile_agent/steering.py +70 -0
- mobile_agent/style.py +178 -0
- mobile_agent/supervised.py +234 -0
- mobile_agent/swift/helper_bt_hid.swift +534 -0
- mobile_agent/swift/helper_doc_text.swift +184 -0
- mobile_agent/swift/helper_draft_reply.swift +170 -0
- mobile_agent/swift/helper_map.swift +110 -0
- mobile_agent/swift/helper_mirroring.swift +1099 -0
- mobile_agent/swift/helper_mjpeg_mp4.swift +103 -0
- mobile_agent/swift/helper_pdf.swift +100 -0
- mobile_agent/swift/helper_phone_audio.swift +993 -0
- mobile_agent/swift/helper_phone_screen.swift +416 -0
- mobile_agent/swift/helper_screen_text.swift +24 -0
- mobile_agent/swift/helper_transfer.swift +246 -0
- mobile_agent/swift/ocr.swift +22 -0
- mobile_agent/task_policy.py +337 -0
- mobile_agent/team_signing/__init__.py +34 -0
- mobile_agent/team_signing/apple_ca.py +37 -0
- mobile_agent/team_signing/bundle.py +136 -0
- mobile_agent/team_signing/cli.py +208 -0
- mobile_agent/team_signing/identity.py +248 -0
- mobile_agent/team_signing/ledger.py +105 -0
- mobile_agent/team_signing/prebuilt.py +61 -0
- mobile_agent/team_signing/resign.py +256 -0
- mobile_agent/team_signing/service.py +382 -0
- mobile_agent/team_signing/tooling.py +243 -0
- mobile_agent/team_signing/writes.py +268 -0
- mobile_agent/terminal_image.py +81 -0
- mobile_agent/testkit/__init__.py +1 -0
- mobile_agent/testkit/a11y_baseline.py +132 -0
- mobile_agent/testkit/api.py +403 -0
- mobile_agent/testkit/capsule.py +400 -0
- mobile_agent/testkit/capsule_cli.py +57 -0
- mobile_agent/testkit/cli.py +948 -0
- mobile_agent/testkit/cli_arguments.py +193 -0
- mobile_agent/testkit/comparison.py +288 -0
- mobile_agent/testkit/comparison_cli.py +41 -0
- mobile_agent/testkit/discover.py +257 -0
- mobile_agent/testkit/editor.py +243 -0
- mobile_agent/testkit/execute.py +515 -0
- mobile_agent/testkit/github_checks.py +248 -0
- mobile_agent/testkit/gitref.py +66 -0
- mobile_agent/testkit/heal_review.py +176 -0
- mobile_agent/testkit/health.py +480 -0
- mobile_agent/testkit/history.py +255 -0
- mobile_agent/testkit/hosted_results.py +321 -0
- mobile_agent/testkit/input_files.py +173 -0
- mobile_agent/testkit/jobs.py +285 -0
- mobile_agent/testkit/junit_verdict.py +33 -0
- mobile_agent/testkit/maestro.py +802 -0
- mobile_agent/testkit/mcp.py +562 -0
- mobile_agent/testkit/mcp_comparison.py +88 -0
- mobile_agent/testkit/minimize.py +471 -0
- mobile_agent/testkit/minimizer_cli.py +48 -0
- mobile_agent/testkit/notify.py +212 -0
- mobile_agent/testkit/phone_video.py +191 -0
- mobile_agent/testkit/pr.py +491 -0
- mobile_agent/testkit/pr_report.py +562 -0
- mobile_agent/testkit/prbuild.py +148 -0
- mobile_agent/testkit/proof_report.py +55 -0
- mobile_agent/testkit/provenance.py +119 -0
- mobile_agent/testkit/prrun.py +244 -0
- mobile_agent/testkit/recent.py +166 -0
- mobile_agent/testkit/recipes.py +146 -0
- mobile_agent/testkit/record.py +204 -0
- mobile_agent/testkit/register.py +288 -0
- mobile_agent/testkit/reports.py +998 -0
- mobile_agent/testkit/repro.py +432 -0
- mobile_agent/testkit/scenario.py +440 -0
- mobile_agent/testkit/scenario_cli.py +37 -0
- mobile_agent/testkit/snapshots_cli.py +60 -0
- mobile_agent/testkit/suite.py +670 -0
- mobile_agent/testkit/targets.py +497 -0
- mobile_agent/testkit/xctest.py +378 -0
- mobile_agent/threads/__init__.py +1 -0
- mobile_agent/threads/chat_once.py +264 -0
- mobile_agent/threads/cli.py +740 -0
- mobile_agent/threads/client.py +326 -0
- mobile_agent/threads/context.py +246 -0
- mobile_agent/threads/mcp.py +739 -0
- mobile_agent/threads/recent.py +34 -0
- mobile_agent/threads/register.py +54 -0
- mobile_agent/threads/routes.py +188 -0
- mobile_agent/threads/service.py +975 -0
- mobile_agent/threads/store.py +361 -0
- mobile_agent/threads/summary.py +117 -0
- mobile_agent/threads/term.py +101 -0
- mobile_agent/threads/workflow_cli.py +104 -0
- mobile_agent/tls.py +36 -0
- mobile_agent/traces.py +271 -0
- mobile_agent/tracks.py +235 -0
- mobile_agent/transport.py +271 -0
- mobile_agent/triggers/__init__.py +1 -0
- mobile_agent/triggers/actions.py +204 -0
- mobile_agent/triggers/model.py +146 -0
- mobile_agent/triggers/register.py +29 -0
- mobile_agent/triggers/routes.py +101 -0
- mobile_agent/triggers/service.py +387 -0
- mobile_agent/triggers/store.py +142 -0
- mobile_agent/tui/__init__.py +89 -0
- mobile_agent/tui/app.py +582 -0
- mobile_agent/tui/chrome.py +321 -0
- mobile_agent/tui/commands.py +375 -0
- mobile_agent/tui/demo_phone.py +369 -0
- mobile_agent/tui/images.py +29 -0
- mobile_agent/tui/keys.py +64 -0
- mobile_agent/tui/prompt.py +195 -0
- mobile_agent/tui/render.py +395 -0
- mobile_agent/tui/screens.py +180 -0
- mobile_agent/tui/session.py +308 -0
- mobile_agent/tui/tasks.py +287 -0
- mobile_agent/tui/theme.py +118 -0
- mobile_agent/tui/widgets.py +272 -0
- mobile_agent/update_check.py +213 -0
- mobile_agent/usage.py +184 -0
- mobile_agent/verify/__init__.py +0 -0
- mobile_agent/verify/__main__.py +41 -0
- mobile_agent/verify/a11y.py +189 -0
- mobile_agent/verify/actions.py +290 -0
- mobile_agent/verify/assertions.py +570 -0
- mobile_agent/verify/audit.py +312 -0
- mobile_agent/verify/business.py +221 -0
- mobile_agent/verify/business_run.py +131 -0
- mobile_agent/verify/business_state.py +168 -0
- mobile_agent/verify/check_schema.py +398 -0
- mobile_agent/verify/checks.py +614 -0
- mobile_agent/verify/cli.py +423 -0
- mobile_agent/verify/conditions.py +483 -0
- mobile_agent/verify/crashes.py +321 -0
- mobile_agent/verify/device_audit.py +246 -0
- mobile_agent/verify/dialogs.py +287 -0
- mobile_agent/verify/evidence.py +705 -0
- mobile_agent/verify/fixtures.py +305 -0
- mobile_agent/verify/flow.py +213 -0
- mobile_agent/verify/l10n.py +141 -0
- mobile_agent/verify/netcerts.py +67 -0
- mobile_agent/verify/netproxy.py +356 -0
- mobile_agent/verify/netrun.py +81 -0
- mobile_agent/verify/netshim.m +28 -0
- mobile_agent/verify/netshim.py +48 -0
- mobile_agent/verify/netstubs.py +132 -0
- mobile_agent/verify/overlay.py +174 -0
- mobile_agent/verify/perf.py +239 -0
- mobile_agent/verify/proof.py +192 -0
- mobile_agent/verify/replay_admission.py +48 -0
- mobile_agent/verify/report.py +952 -0
- mobile_agent/verify/run_cleanup.py +67 -0
- mobile_agent/verify/run_links.py +48 -0
- mobile_agent/verify/run_perf.py +44 -0
- mobile_agent/verify/run_secrets.py +39 -0
- mobile_agent/verify/runner.py +976 -0
- mobile_agent/verify/runs.py +57 -0
- mobile_agent/verify/screen_coverage.py +181 -0
- mobile_agent/verify/smart.py +106 -0
- mobile_agent/verify/snapshot_run.py +114 -0
- mobile_agent/verify/snapshots.py +306 -0
- mobile_agent/verify/sound.py +335 -0
- mobile_agent/verify/source_observation.py +130 -0
- mobile_agent/verify/system_surfaces/__init__.py +7 -0
- mobile_agent/verify/system_surfaces/play.py +106 -0
- mobile_agent/verify/system_surfaces/spec.py +139 -0
- mobile_agent/verify/system_surfaces/stage.py +138 -0
- mobile_agent/verify/system_surfaces/surfaces.py +121 -0
- mobile_agent/verify/tree.py +223 -0
- mobile_agent/verify/verdict.py +377 -0
- mobile_agent/verify/wire.py +392 -0
- mobile_agent/verify/wirestore.py +176 -0
- mobile_agent/waiting/__init__.py +1 -0
- mobile_agent/waiting/contacts.py +73 -0
- mobile_agent/waiting/drafts.py +136 -0
- mobile_agent/waiting/job.py +145 -0
- mobile_agent/waiting/messages.py +174 -0
- mobile_agent/waiting/rank.py +115 -0
- mobile_agent/waiting/register.py +26 -0
- mobile_agent/watch.py +502 -0
- mobile_agent/wda/__init__.py +2 -0
- mobile_agent/wda/frame_clock.py +646 -0
- mobile_agent/wda/frame_source.py +325 -0
- mobile_agent/wda/frame_store.py +115 -0
- mobile_agent/wda/gate.py +163 -0
- mobile_agent/wda/gestures.py +291 -0
- mobile_agent/wda/guards.py +94 -0
- mobile_agent/wda/identity.py +55 -0
- mobile_agent/wda/observe.py +884 -0
- mobile_agent/wda/patches.py +856 -0
- mobile_agent/wda/point_gestures.py +150 -0
- mobile_agent/wda/source.py +32 -0
- mobile_agent/wda/system_dialogs.py +571 -0
- mobile_agent/wda/text.py +694 -0
- mobile_agent/wda/timeouts.py +77 -0
- mobile_agent/wda/video.py +505 -0
- mobile_agent/wda/watchdog.py +243 -0
- mobile_agent/wireless/__init__.py +15 -0
- mobile_agent/wireless/api.py +354 -0
- mobile_agent/wireless/cli.py +287 -0
- mobile_agent/wireless/devicectl.py +229 -0
- mobile_agent/wireless/exposure.py +33 -0
- mobile_agent/wireless/mobster-wifi-pair.c +122 -0
- mobile_agent/wireless/monitor.py +332 -0
- mobile_agent/wireless/network.py +334 -0
- mobile_agent/wireless/onboarding.py +293 -0
- mobile_agent/wireless/pair.py +124 -0
- mobile_agent/wireless/probe.py +322 -0
- mobile_agent/wireless/register.py +8 -0
- mobile_agent/wireless/relay.py +179 -0
- mobile_agent/wireless/result.py +87 -0
- mobile_agent/wireless/store.py +85 -0
- mobile_agent/wireless/switch.py +77 -0
- mobile_agent/wireless/transport.py +118 -0
- mobile_agent/wireless/words.py +81 -0
- mobile_agent/wireless/xctestrun.py +83 -0
- mobile_agent/workflow_tools.py +59 -0
- mobile_agent/workflows/__init__.py +2 -0
- mobile_agent/workflows/brief.py +201 -0
- mobile_agent/workflows/cron.py +110 -0
- mobile_agent/workflows/health.py +257 -0
- mobile_agent/workflows/history.py +126 -0
- mobile_agent/workflows/records.py +442 -0
- mobile_agent/workflows/runs.py +1000 -0
- mobile_agent/workflows/scheduler.py +134 -0
- mobile_agent/workflows/service.py +973 -0
- mobile_agent/workflows/starters.py +285 -0
- mobile_agent/workflows/triggers.py +248 -0
- mobile_agent/xray/__init__.py +1 -0
- mobile_agent/xray/app_cli.py +116 -0
- mobile_agent/xray/cli.py +21 -0
- mobile_agent/xray/crawl.py +754 -0
- mobile_agent/xray/diff.py +171 -0
- mobile_agent/xray/diff_cli.py +43 -0
- mobile_agent/xray/job.py +184 -0
- mobile_agent/xray/paywall.py +188 -0
- mobile_agent/xray/policy.py +305 -0
- mobile_agent/xray/register.py +43 -0
- mobile_agent/xray/report.py +283 -0
- mobile_agent/xray/screens.py +117 -0
- mobster_cli-0.4.3.dist-info/METADATA +93 -0
- mobster_cli-0.4.3.dist-info/RECORD +1009 -0
- mobster_cli-0.4.3.dist-info/WHEEL +5 -0
- mobster_cli-0.4.3.dist-info/entry_points.txt +3 -0
- mobster_cli-0.4.3.dist-info/licenses/LICENSE +21 -0
- mobster_cli-0.4.3.dist-info/top_level.txt +1 -0
mobile_agent/sim/wda.py
ADDED
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
"""WebDriverAgent on Mobster's simulators: the pinned source, one build per Xcode, and one runner per simulator."""
|
|
2
|
+
|
|
3
|
+
import atexit
|
|
4
|
+
import contextlib
|
|
5
|
+
import errno
|
|
6
|
+
import fcntl
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import shutil
|
|
12
|
+
import signal
|
|
13
|
+
import subprocess
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
16
|
+
import urllib.error
|
|
17
|
+
import urllib.request
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import NamedTuple
|
|
20
|
+
|
|
21
|
+
from ..devices import LOOPBACK, wda_url
|
|
22
|
+
from ..wda import gate as wda_gate
|
|
23
|
+
from ..wda import patches as wda_patches
|
|
24
|
+
from ..wda import source as wda_source
|
|
25
|
+
from ..wda.patches import pin_loopback, xctestrun_targets
|
|
26
|
+
from .models import SimError
|
|
27
|
+
from .simctl import run as run_command
|
|
28
|
+
|
|
29
|
+
COMMIT8 = wda_source.COMMIT[:8]
|
|
30
|
+
PATCH_DIGEST = hashlib.sha256(repr(wda_patches.active_patches()).encode()).hexdigest()[:8] # the flagged routes too
|
|
31
|
+
BUILD_TIMEOUT = 1200
|
|
32
|
+
CLONE_TIMEOUT = 600
|
|
33
|
+
READY_TIMEOUT = 240 # a busy Mac took over 120 s to start a runner (smoke, 28 Sep, load average 780)
|
|
34
|
+
READY_POLL = .5
|
|
35
|
+
BUILD_LOCK_TIMEOUT = 1500
|
|
36
|
+
MJPEG_WAIT = 5.0 # the MJPEG socket opens a moment after /status answers
|
|
37
|
+
HTTP_TIMEOUT = 10.0
|
|
38
|
+
DEVICE_PATH = re.compile(r"/CoreSimulator/Devices/([0-9A-Fa-f-]{36})/")
|
|
39
|
+
HEARTBEAT_SECONDS = 30.0
|
|
40
|
+
STOP_GRACE_SECONDS = 1 # the runner is dead or wedged: its xcodebuild is not waited on long
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def elapsed_words(seconds):
|
|
44
|
+
""""45 s", "2 min 05 s"."""
|
|
45
|
+
seconds = int(max(0, seconds))
|
|
46
|
+
return f"{seconds} s" if seconds < 60 else f"{seconds // 60} min {seconds % 60:02d} s"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@contextlib.contextmanager
|
|
50
|
+
def heartbeat(progress, what, interval=None, clock=time.monotonic):
|
|
51
|
+
interval = HEARTBEAT_SECONDS if interval is None else interval
|
|
52
|
+
done = threading.Event()
|
|
53
|
+
started = clock()
|
|
54
|
+
|
|
55
|
+
def beat():
|
|
56
|
+
while not done.wait(interval):
|
|
57
|
+
try:
|
|
58
|
+
progress(f"Still {what} ({elapsed_words(clock() - started)} so far)")
|
|
59
|
+
except Exception:
|
|
60
|
+
return
|
|
61
|
+
thread = threading.Thread(target=beat, name="mobster-heartbeat", daemon=True)
|
|
62
|
+
thread.start()
|
|
63
|
+
try:
|
|
64
|
+
yield
|
|
65
|
+
finally:
|
|
66
|
+
done.set()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# Every build or clone ``run_logged`` has running, across WebDriverAgent instances. Each runs in its own process
|
|
70
|
+
# group (start_new_session), so neither ctrl+c nor the end of this process reaches it: the exit hook below kills
|
|
71
|
+
# what is left, which covers a build on a daemon thread when the interpreter exits under it.
|
|
72
|
+
_RUNNING = set()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@atexit.register
|
|
76
|
+
def _kill_running():
|
|
77
|
+
for process in list(_RUNNING):
|
|
78
|
+
_kill_group(process)
|
|
79
|
+
_RUNNING.clear()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# The simulator runners this process started (xcodebuild test-without-building), each in its own session so a
|
|
83
|
+
# crash or ctrl+c of a command leaves the warm simulator's runner for the next command. A process that owns them for
|
|
84
|
+
# its whole life (`mobster serve`) calls stop_started() as it closes, or the runner outlives it and boots the
|
|
85
|
+
# simulator again after `simctl shutdown`.
|
|
86
|
+
_STARTED = set()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def stop_started():
|
|
90
|
+
"""Stop every runner this process started and that is still running (their process groups)."""
|
|
91
|
+
for process in list(_STARTED):
|
|
92
|
+
_STARTED.discard(process)
|
|
93
|
+
if process.poll() is None:
|
|
94
|
+
_kill_group(process)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def source_dir(data_dir):
|
|
98
|
+
return Path(data_dir) / "wda" / f"src-{COMMIT8}"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def build_dir(data_dir, xcode_build):
|
|
102
|
+
return Path(data_dir) / "wda" / f"build-{xcode_build}-{COMMIT8}-{PATCH_DIGEST}"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def parse_build_dir(name):
|
|
106
|
+
"""(Xcode build, commit8, patch digest) from a build folder's name, or None."""
|
|
107
|
+
match = re.fullmatch(r"build-([^-]+)-([0-9a-f]{8})-([0-9a-f]{8})", name)
|
|
108
|
+
return match.groups() if match else None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def http_json(method, url, body=None, timeout=HTTP_TIMEOUT):
|
|
112
|
+
data = None if body is None else json.dumps(body).encode()
|
|
113
|
+
request = urllib.request.Request(url, data=data, method=method,
|
|
114
|
+
headers={"Content-Type": "application/json"} if data is not None else {})
|
|
115
|
+
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
|
116
|
+
try:
|
|
117
|
+
with opener.open(request, timeout=timeout) as response:
|
|
118
|
+
status, raw = response.status, response.read()
|
|
119
|
+
except urllib.error.HTTPError as exc:
|
|
120
|
+
try:
|
|
121
|
+
status, raw = exc.code, exc.read()
|
|
122
|
+
finally:
|
|
123
|
+
exc.close()
|
|
124
|
+
except (OSError, ValueError):
|
|
125
|
+
return None, None
|
|
126
|
+
try:
|
|
127
|
+
payload = json.loads(raw.decode("utf-8", "replace") or "null")
|
|
128
|
+
except ValueError:
|
|
129
|
+
payload = None
|
|
130
|
+
return status, (payload.get("value") if isinstance(payload, dict) else payload)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def find_xctestrun(build):
|
|
134
|
+
"""The simulator test plan a finished build leaves, or None."""
|
|
135
|
+
products = Path(build) / "Build" / "Products"
|
|
136
|
+
try:
|
|
137
|
+
plans = sorted(products.glob("*iphonesimulator*.xctestrun"), key=lambda path: path.stat().st_mtime)
|
|
138
|
+
except OSError:
|
|
139
|
+
return None
|
|
140
|
+
return plans[-1] if plans else None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# -- lsof and ps -------------------------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
class Listener(NamedTuple):
|
|
146
|
+
command: str
|
|
147
|
+
pid: int
|
|
148
|
+
host: str # "127.0.0.1", "*", "::1", "192.168.1.4"
|
|
149
|
+
port: int
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def parse_lsof(text):
|
|
153
|
+
"""Listening sockets from `lsof -nP -iTCP:<port> -sTCP:LISTEN` (its default table)."""
|
|
154
|
+
listeners = []
|
|
155
|
+
for line in (text or "").splitlines():
|
|
156
|
+
tokens = line.split()
|
|
157
|
+
if len(tokens) < 9 or tokens[0] == "COMMAND" or not tokens[1].isdigit():
|
|
158
|
+
continue
|
|
159
|
+
name = tokens[-2] if tokens[-1] == "(LISTEN)" else tokens[-1]
|
|
160
|
+
host, _, port = name.rpartition(":")
|
|
161
|
+
if not port.isdigit():
|
|
162
|
+
continue
|
|
163
|
+
host = host[1:-1] if host.startswith("[") and host.endswith("]") else host
|
|
164
|
+
listeners.append(Listener(tokens[0], int(tokens[1]), host or "*", int(port)))
|
|
165
|
+
return listeners
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def loopback_only(listeners):
|
|
169
|
+
"""True when something listens, and only on 127.0.0.1."""
|
|
170
|
+
return bool(listeners) and all(listener.host == LOOPBACK for listener in listeners)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def device_of(command):
|
|
174
|
+
"""The UDID a simulator process runs in, from its executable's path, or None."""
|
|
175
|
+
match = DEVICE_PATH.search(command or "")
|
|
176
|
+
return match.group(1).upper() if match else None
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def runner_commands(udid, ps_output):
|
|
180
|
+
"""[(pid, command)] of the xcodebuild runs driving this simulator's runner."""
|
|
181
|
+
runners = []
|
|
182
|
+
for line in (ps_output or "").splitlines():
|
|
183
|
+
pid, _, command = line.strip().partition(" ")
|
|
184
|
+
if ("xcodebuild" in command and "test-without-building" in command and f"id={udid}" in command
|
|
185
|
+
and pid.isdigit()):
|
|
186
|
+
runners.append((int(pid), command.strip()))
|
|
187
|
+
return runners
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def runner_pids(udid, ps_output=None):
|
|
191
|
+
"""PIDs of the xcodebuild test runs whose destination is this simulator (`ps` is read unless given)."""
|
|
192
|
+
if ps_output is None:
|
|
193
|
+
ps_output = subprocess.run(["ps", "-axo", "pid=,command="], capture_output=True, text=True,
|
|
194
|
+
timeout=10).stdout
|
|
195
|
+
return [pid for pid, _ in runner_commands(udid, ps_output)]
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def runner_command(xcodebuild, xctestrun, udid):
|
|
199
|
+
"""The command that runs a built runner on one simulator."""
|
|
200
|
+
return [xcodebuild, "test-without-building", "-xctestrun", str(xctestrun), "-destination", f"id={udid}"]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def runner_env(base, port, mjpeg):
|
|
204
|
+
"""``base`` plus the runner's ports, 127.0.0.1 only whatever the test plan says, and the actuation gate."""
|
|
205
|
+
return {**base, "TEST_RUNNER_USE_PORT": str(port), "TEST_RUNNER_MJPEG_SERVER_PORT": str(mjpeg),
|
|
206
|
+
"TEST_RUNNER_USE_IP": LOOPBACK, **wda_gate.runner_env("simulator")}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def ready(url, timeout=2):
|
|
210
|
+
"""True when the runner at ``url`` answers /status as ready."""
|
|
211
|
+
try:
|
|
212
|
+
with urllib.request.urlopen(url.rstrip("/") + "/status", timeout=timeout) as response:
|
|
213
|
+
value = json.load(response).get("value") or {}
|
|
214
|
+
return value.get("ready", True) is not False
|
|
215
|
+
except (OSError, ValueError, AttributeError):
|
|
216
|
+
return False
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _alive(pid):
|
|
220
|
+
try:
|
|
221
|
+
os.kill(pid, 0)
|
|
222
|
+
except OSError:
|
|
223
|
+
return False
|
|
224
|
+
return True
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _signal(pid, number):
|
|
228
|
+
# a runner started by hand shares its shell's process group with other simulators' runners (26 Sep)
|
|
229
|
+
try:
|
|
230
|
+
if os.getpgid(pid) == pid:
|
|
231
|
+
os.killpg(pid, number)
|
|
232
|
+
else:
|
|
233
|
+
os.kill(pid, number)
|
|
234
|
+
except OSError:
|
|
235
|
+
pass
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def terminate(pids, clock=time.monotonic, sleep=time.sleep):
|
|
239
|
+
"""SIGTERM these runners, give them STOP_GRACE_SECONDS, then SIGKILL what is left; returns ``pids``."""
|
|
240
|
+
for pid in pids:
|
|
241
|
+
_signal(pid, signal.SIGTERM)
|
|
242
|
+
end = clock() + STOP_GRACE_SECONDS
|
|
243
|
+
while clock() < end and any(_alive(pid) for pid in pids):
|
|
244
|
+
sleep(.2)
|
|
245
|
+
for pid in pids:
|
|
246
|
+
if _alive(pid):
|
|
247
|
+
_signal(pid, signal.SIGKILL)
|
|
248
|
+
return pids
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def xctestrun_of(command):
|
|
252
|
+
"""The test plan an xcodebuild runner was started with (paths may hold spaces)."""
|
|
253
|
+
match = re.search(r" -xctestrun (.+?\.xctestrun)(?= |$)", command or "")
|
|
254
|
+
return match.group(1) if match else None
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
REUSE, RESTART, MOVE, START = "reuse", "restart", "move", "start"
|
|
258
|
+
UNCHECKED = "unchecked" # the owner lookup (lsof) was skipped: this simulator's own runner answers
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def runner_plan(*, udid, listening, owner, ready, runners, xctestrun):
|
|
262
|
+
current = any(xctestrun_of(command) == str(xctestrun) for _, command in runners)
|
|
263
|
+
if listening and ready and current and owner in (udid, UNCHECKED):
|
|
264
|
+
return REUSE
|
|
265
|
+
# something else listens on the port: take another pair, never adopt a stranger's server
|
|
266
|
+
if listening and owner not in (udid, UNCHECKED):
|
|
267
|
+
return MOVE
|
|
268
|
+
if listening:
|
|
269
|
+
return RESTART
|
|
270
|
+
return RESTART if runners else START
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class WebDriverAgent:
|
|
274
|
+
"""Build, start, stop and check simulator runners."""
|
|
275
|
+
|
|
276
|
+
SEAMS = ("run", "ready", "listening", "popen", "sleep", "clock", "http")
|
|
277
|
+
|
|
278
|
+
def __init__(self, data_dir, *, log=None, progress=None):
|
|
279
|
+
from .registry import listening
|
|
280
|
+
self.data_dir = Path(data_dir)
|
|
281
|
+
self.logs = self.data_dir / "logs"
|
|
282
|
+
self.log = log or (lambda message: None)
|
|
283
|
+
self.progress = progress or (lambda message: None)
|
|
284
|
+
self.run = run_command
|
|
285
|
+
self.ready = ready
|
|
286
|
+
self.listening = listening
|
|
287
|
+
self.popen = subprocess.Popen
|
|
288
|
+
self.sleep = time.sleep
|
|
289
|
+
self.clock = time.monotonic
|
|
290
|
+
self.http = http_json
|
|
291
|
+
self._running = set()
|
|
292
|
+
|
|
293
|
+
def ps(self):
|
|
294
|
+
return self.run(["/bin/ps", "-axo", "pid=,command="], timeout=10).out
|
|
295
|
+
|
|
296
|
+
def lsof(self, port):
|
|
297
|
+
return self.run(["/usr/sbin/lsof", "-nP", f"-iTCP:{int(port)}", "-sTCP:LISTEN"], timeout=10).out
|
|
298
|
+
|
|
299
|
+
# -- source and build ----------------------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
@contextlib.contextmanager
|
|
302
|
+
def build_lock(self):
|
|
303
|
+
"""One build or clone at a time across processes; the second waits and then finds it built."""
|
|
304
|
+
folder = self.data_dir / "wda"
|
|
305
|
+
folder.mkdir(parents=True, exist_ok=True)
|
|
306
|
+
fd = os.open(folder / "build.lock", os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
|
|
307
|
+
try:
|
|
308
|
+
end, told = self.clock() + BUILD_LOCK_TIMEOUT, False
|
|
309
|
+
while True:
|
|
310
|
+
try:
|
|
311
|
+
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
312
|
+
break
|
|
313
|
+
except OSError as exc:
|
|
314
|
+
if exc.errno not in (errno.EWOULDBLOCK, errno.EAGAIN) or self.clock() >= end:
|
|
315
|
+
raise SimError("busy", "Another Mobster process is still building WebDriverAgent.",
|
|
316
|
+
"Wait for it to finish, then try again.") from None
|
|
317
|
+
if not told:
|
|
318
|
+
self.progress("Waiting for another Mobster process to finish building WebDriverAgent")
|
|
319
|
+
told = True
|
|
320
|
+
self.sleep(1)
|
|
321
|
+
yield
|
|
322
|
+
finally:
|
|
323
|
+
os.close(fd)
|
|
324
|
+
|
|
325
|
+
def ensure_source(self, tools):
|
|
326
|
+
"""The pinned clone, patched. Cloned into a temporary folder and renamed, so a half clone never counts."""
|
|
327
|
+
source = source_dir(self.data_dir)
|
|
328
|
+
if not tools.git:
|
|
329
|
+
raise SimError("environment", "git isn't installed, and building WebDriverAgent needs it.",
|
|
330
|
+
"Install Xcode, which includes git.")
|
|
331
|
+
if source.is_dir() and wda_source.is_pinned(tools.git, source):
|
|
332
|
+
self._patch(source)
|
|
333
|
+
return source
|
|
334
|
+
if source.exists():
|
|
335
|
+
shutil.rmtree(source)
|
|
336
|
+
temporary = source.with_name(f".{source.name}-{os.getpid()}")
|
|
337
|
+
if temporary.exists():
|
|
338
|
+
shutil.rmtree(temporary)
|
|
339
|
+
self.progress(f"Downloading WebDriverAgent {wda_source.REF}")
|
|
340
|
+
with heartbeat(self.progress, "downloading WebDriverAgent"):
|
|
341
|
+
result = self.run(wda_source.clone_command(tools.git, temporary), timeout=CLONE_TIMEOUT, env=tools.env)
|
|
342
|
+
if not result.ok:
|
|
343
|
+
shutil.rmtree(temporary, ignore_errors=True)
|
|
344
|
+
raise SimError("wda", f"Couldn't download WebDriverAgent {wda_source.REF} from GitHub: "
|
|
345
|
+
f"{result.last_line() or 'git failed'}",
|
|
346
|
+
"Check this Mac's network connection, then try again.")
|
|
347
|
+
try:
|
|
348
|
+
wda_source.verify(tools.git, temporary)
|
|
349
|
+
except RuntimeError as exc:
|
|
350
|
+
shutil.rmtree(temporary, ignore_errors=True)
|
|
351
|
+
raise SimError("wda", str(exc), "Try again; if it repeats, the release was moved upstream.") from None
|
|
352
|
+
os.replace(temporary, source)
|
|
353
|
+
self._patch(source)
|
|
354
|
+
return source
|
|
355
|
+
|
|
356
|
+
def _patch(self, source):
|
|
357
|
+
try:
|
|
358
|
+
wda_patches.patch_wda(source)
|
|
359
|
+
except (OSError, RuntimeError):
|
|
360
|
+
raise SimError("wda", "WebDriverAgent's source doesn't take Mobster's loopback patches.",
|
|
361
|
+
f"Delete {source} and try again.") from None
|
|
362
|
+
|
|
363
|
+
def ensure_build(self, tools):
|
|
364
|
+
build = build_dir(self.data_dir, tools.build)
|
|
365
|
+
plan = find_xctestrun(build)
|
|
366
|
+
if plan and plan_is_pinned(plan):
|
|
367
|
+
return plan
|
|
368
|
+
with self.build_lock():
|
|
369
|
+
plan = find_xctestrun(build)
|
|
370
|
+
if plan:
|
|
371
|
+
# a process that died between xcodebuild writing the plan and the pin left it listening everywhere
|
|
372
|
+
if not plan_is_pinned(plan):
|
|
373
|
+
pin_loopback(plan.parent)
|
|
374
|
+
self.log(f"pinned the cached test plan to {LOOPBACK}: {plan}")
|
|
375
|
+
return plan
|
|
376
|
+
source = self.ensure_source(tools)
|
|
377
|
+
self.logs.mkdir(parents=True, exist_ok=True)
|
|
378
|
+
log = self.logs / "wda-build.log"
|
|
379
|
+
self.progress(f"Building WebDriverAgent for Xcode {tools.version}, once per Xcode version: usually 2 to "
|
|
380
|
+
f"5 minutes (log: {log})")
|
|
381
|
+
command = [tools.xcodebuild, "build-for-testing", "-project", str(source / "WebDriverAgent.xcodeproj"),
|
|
382
|
+
"-scheme", "WebDriverAgentRunner", "-destination", "generic/platform=iOS Simulator",
|
|
383
|
+
"-derivedDataPath", str(build), "CODE_SIGNING_ALLOWED=NO"]
|
|
384
|
+
started = self.clock()
|
|
385
|
+
with heartbeat(self.progress, "building WebDriverAgent"):
|
|
386
|
+
code = self.run_logged(command, log, BUILD_TIMEOUT, tools.env)
|
|
387
|
+
plan = find_xctestrun(build)
|
|
388
|
+
if code != 0 or not plan:
|
|
389
|
+
raise SimError("wda", f"WebDriverAgent didn't build: {last_error(log) or 'xcodebuild failed'}",
|
|
390
|
+
f"See {log}.")
|
|
391
|
+
pin_loopback(build / "Build" / "Products")
|
|
392
|
+
self.log(f"built WebDriverAgent in {self.clock() - started:.1f} s: {plan}")
|
|
393
|
+
return plan
|
|
394
|
+
|
|
395
|
+
def run_logged(self, command, log, timeout, env):
|
|
396
|
+
with open(log, "ab") as stream:
|
|
397
|
+
stream.write(f"\n$ {' '.join(map(str, command))}\n".encode())
|
|
398
|
+
stream.flush()
|
|
399
|
+
try:
|
|
400
|
+
process = subprocess.Popen(command, stdout=stream, stderr=subprocess.STDOUT,
|
|
401
|
+
stdin=subprocess.DEVNULL, env=env, start_new_session=True)
|
|
402
|
+
except OSError as exc:
|
|
403
|
+
stream.write(f"\nerror: {exc}\n".encode())
|
|
404
|
+
return None
|
|
405
|
+
self._running.add(process)
|
|
406
|
+
_RUNNING.add(process)
|
|
407
|
+
try:
|
|
408
|
+
return process.wait(timeout=timeout)
|
|
409
|
+
except subprocess.TimeoutExpired:
|
|
410
|
+
_kill_group(process)
|
|
411
|
+
stream.write(f"\nerror: did not finish in {timeout} s\n".encode())
|
|
412
|
+
return None
|
|
413
|
+
except BaseException:
|
|
414
|
+
_kill_group(process)
|
|
415
|
+
raise
|
|
416
|
+
finally:
|
|
417
|
+
self._running.discard(process)
|
|
418
|
+
_RUNNING.discard(process)
|
|
419
|
+
|
|
420
|
+
def cancel(self):
|
|
421
|
+
"""Stop a build running in another thread (the acquire that started it failed or was interrupted)."""
|
|
422
|
+
for process in list(self._running):
|
|
423
|
+
_kill_group(process)
|
|
424
|
+
|
|
425
|
+
# -- runners -------------------------------------------------------------------------------------------
|
|
426
|
+
|
|
427
|
+
def runner_log(self, udid):
|
|
428
|
+
return self.logs / f"wda-{udid}.log"
|
|
429
|
+
|
|
430
|
+
def owner(self, port):
|
|
431
|
+
"""The UDID of the simulator whose process listens on ``port``, or None."""
|
|
432
|
+
pids = {listener.pid for listener in parse_lsof(self.lsof(port))}
|
|
433
|
+
for pid in sorted(pids):
|
|
434
|
+
command = self.run(["/bin/ps", "-o", "command=", "-p", str(pid)], timeout=10).out
|
|
435
|
+
udid = device_of(command)
|
|
436
|
+
if udid:
|
|
437
|
+
return udid
|
|
438
|
+
return None
|
|
439
|
+
|
|
440
|
+
def plan(self, udid, port, mjpeg, xctestrun):
|
|
441
|
+
url = wda_url(port)
|
|
442
|
+
runners = runner_commands(udid, self.ps())
|
|
443
|
+
current = any(xctestrun_of(command) == str(xctestrun) for _, command in runners)
|
|
444
|
+
busy_ports = [number for number in (port, mjpeg) if self.listening(number)]
|
|
445
|
+
ready = port in busy_ports and self.ready(url)
|
|
446
|
+
owner = None
|
|
447
|
+
if busy_ports and ready and current:
|
|
448
|
+
owner = UNCHECKED
|
|
449
|
+
elif busy_ports:
|
|
450
|
+
owners = {self.owner(number) for number in busy_ports}
|
|
451
|
+
owner = udid if owners == {udid} else next(iter(owners - {udid})) # a stranger on either port
|
|
452
|
+
return runner_plan(udid=udid, listening=bool(busy_ports), owner=owner, ready=ready, runners=runners,
|
|
453
|
+
xctestrun=xctestrun)
|
|
454
|
+
|
|
455
|
+
def start(self, udid, port, mjpeg, xctestrun, tools):
|
|
456
|
+
"""Start the runner in its own session, output to logs/wda-<udid>.log."""
|
|
457
|
+
self.logs.mkdir(parents=True, exist_ok=True)
|
|
458
|
+
env = runner_env(tools.env, port, mjpeg)
|
|
459
|
+
gate = env[wda_gate.RUNNER_VARIABLE]
|
|
460
|
+
command = runner_command(tools.xcodebuild, xctestrun, udid)
|
|
461
|
+
with open(self.runner_log(udid), "ab") as stream:
|
|
462
|
+
stream.write(f"\n$ TEST_RUNNER_USE_PORT={port} TEST_RUNNER_MJPEG_SERVER_PORT={mjpeg} "
|
|
463
|
+
f"TEST_RUNNER_USE_IP={LOOPBACK} {wda_gate.RUNNER_VARIABLE}={gate} "
|
|
464
|
+
f"{' '.join(command)}\n".encode())
|
|
465
|
+
stream.flush()
|
|
466
|
+
process = self.popen(command, cwd=str(Path(xctestrun).parent), env=env, stdout=stream,
|
|
467
|
+
stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL, start_new_session=True)
|
|
468
|
+
_STARTED.add(process)
|
|
469
|
+
return process
|
|
470
|
+
|
|
471
|
+
def wait_ready(self, udid, port, process=None, timeout=READY_TIMEOUT):
|
|
472
|
+
with heartbeat(self.progress, f"waiting for WebDriverAgent to answer (log: {self.runner_log(udid)})"):
|
|
473
|
+
return self._wait_ready(udid, port, process, timeout)
|
|
474
|
+
|
|
475
|
+
def _wait_ready(self, udid, port, process, timeout):
|
|
476
|
+
url = wda_url(port)
|
|
477
|
+
end = self.clock() + timeout
|
|
478
|
+
while self.clock() < end:
|
|
479
|
+
if self.ready(url):
|
|
480
|
+
return
|
|
481
|
+
if process is not None and process.poll() is not None:
|
|
482
|
+
raise SimError("wda", "WebDriverAgent stopped before it was ready: "
|
|
483
|
+
f"{last_error(self.runner_log(udid)) or 'xcodebuild exited'}",
|
|
484
|
+
f"See {self.runner_log(udid)}.")
|
|
485
|
+
self.sleep(READY_POLL)
|
|
486
|
+
raise SimError("wda", f"WebDriverAgent didn't answer on {url} within {timeout:g} s, which happens when "
|
|
487
|
+
"this Mac is busy.",
|
|
488
|
+
f"Run the command again. If it keeps happening, see {self.runner_log(udid)}.")
|
|
489
|
+
|
|
490
|
+
def stop(self, udid):
|
|
491
|
+
"""Stop this simulator's runner and only it: SIGTERM, a short wait, SIGKILL."""
|
|
492
|
+
return terminate(runner_pids(udid, self.ps()), self.clock, self.sleep)
|
|
493
|
+
|
|
494
|
+
def listeners(self, port):
|
|
495
|
+
return parse_lsof(self.lsof(port))
|
|
496
|
+
|
|
497
|
+
# -- system alerts (sessionless) -----------------------------------------------------------------------
|
|
498
|
+
|
|
499
|
+
def read_alert(self, url):
|
|
500
|
+
status, value = self.http("GET", f"{url}/alert/text")
|
|
501
|
+
return status is not None, (value if status == 200 and isinstance(value, str) and value else None)
|
|
502
|
+
|
|
503
|
+
def alert_text(self, url):
|
|
504
|
+
"""The text of the alert on screen, or None when there is none or the runner didn't answer."""
|
|
505
|
+
return self.read_alert(url)[1]
|
|
506
|
+
|
|
507
|
+
def alert_action(self, url, action, name=None):
|
|
508
|
+
if action not in ("accept", "dismiss"):
|
|
509
|
+
raise ValueError(f"alert action must be accept or dismiss, not {action!r}")
|
|
510
|
+
status, _ = self.http("POST", f"{url}/alert/{action}", {"name": name} if name else {})
|
|
511
|
+
return status == 200 # even when nothing changed: the caller reads the alert again
|
|
512
|
+
|
|
513
|
+
def check_loopback(self, port, mjpeg):
|
|
514
|
+
api = self.listeners(port)
|
|
515
|
+
end = self.clock() + MJPEG_WAIT
|
|
516
|
+
video = self.listeners(mjpeg)
|
|
517
|
+
while not video and self.clock() < end:
|
|
518
|
+
self.sleep(.25)
|
|
519
|
+
video = self.listeners(mjpeg)
|
|
520
|
+
return loopback_only(api) and loopback_only(video), {port: api, mjpeg: video}
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _kill_group(process):
|
|
524
|
+
"""SIGTERM the process's group, then SIGKILL it if it is still there after two seconds."""
|
|
525
|
+
for number, grace in ((signal.SIGTERM, 2.0), (signal.SIGKILL, 0)):
|
|
526
|
+
try:
|
|
527
|
+
os.killpg(process.pid, number)
|
|
528
|
+
except OSError:
|
|
529
|
+
return
|
|
530
|
+
try:
|
|
531
|
+
process.wait(timeout=grace or None)
|
|
532
|
+
return
|
|
533
|
+
except subprocess.TimeoutExpired:
|
|
534
|
+
continue
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def last_error(log, window=65536):
|
|
538
|
+
"""The last `error:` line of a log (or its last line), for a message."""
|
|
539
|
+
try:
|
|
540
|
+
with open(log, "rb") as stream:
|
|
541
|
+
stream.seek(0, os.SEEK_END)
|
|
542
|
+
stream.seek(max(0, stream.tell() - window))
|
|
543
|
+
lines = stream.read().decode(errors="replace").splitlines()
|
|
544
|
+
except OSError:
|
|
545
|
+
return ""
|
|
546
|
+
errors = [line.strip() for line in lines if "error:" in line.lower()]
|
|
547
|
+
tail = errors or [line.strip() for line in lines if line.strip()]
|
|
548
|
+
return tail[-1][:300] if tail else ""
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
class SimulatorWDA:
|
|
552
|
+
"""The Smart loop's WebDriverAgent restarter for one simulator, which never stops another simulator's runner."""
|
|
553
|
+
|
|
554
|
+
READY_POLL_SECONDS = .5
|
|
555
|
+
|
|
556
|
+
def __init__(self, udid, port, mjpeg_port, xctestrun, *, log_path=None, cwd=None, tools=None):
|
|
557
|
+
if not Path(xctestrun).is_file():
|
|
558
|
+
raise FileNotFoundError(f"no xctestrun at {xctestrun}")
|
|
559
|
+
self.udid, self.port, self.mjpeg_port = udid, int(port), int(mjpeg_port)
|
|
560
|
+
self.xctestrun, self.log_path = str(xctestrun), log_path
|
|
561
|
+
self.cwd = cwd or str(Path(xctestrun).resolve().parent)
|
|
562
|
+
self.url = wda_url(self.port)
|
|
563
|
+
self.tools = tools
|
|
564
|
+
self.restarts = 0
|
|
565
|
+
|
|
566
|
+
def command(self):
|
|
567
|
+
return runner_command(self.tools.xcodebuild if self.tools else "xcodebuild", self.xctestrun, self.udid)
|
|
568
|
+
|
|
569
|
+
def environment(self):
|
|
570
|
+
return runner_env(self.tools.env if self.tools else os.environ, self.port, self.mjpeg_port)
|
|
571
|
+
|
|
572
|
+
def stop(self):
|
|
573
|
+
"""Stop this simulator's runner (its xcodebuild), gracefully then by force."""
|
|
574
|
+
return terminate(runner_pids(self.udid))
|
|
575
|
+
|
|
576
|
+
def start(self):
|
|
577
|
+
log = open(self.log_path, "ab") if self.log_path else subprocess.DEVNULL
|
|
578
|
+
try:
|
|
579
|
+
# Its own session: ctrl+c does not reach it. stop_started() does, for a process that owns it.
|
|
580
|
+
process = subprocess.Popen(self.command(), cwd=self.cwd, env=self.environment(), stdout=log,
|
|
581
|
+
stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL, start_new_session=True)
|
|
582
|
+
_STARTED.add(process)
|
|
583
|
+
return process
|
|
584
|
+
finally:
|
|
585
|
+
if log is not subprocess.DEVNULL:
|
|
586
|
+
log.close()
|
|
587
|
+
|
|
588
|
+
def __call__(self, timeout=90):
|
|
589
|
+
"""Restart the runner; returns once it answers /status, or raises TimeoutError."""
|
|
590
|
+
started = time.monotonic()
|
|
591
|
+
self.stop()
|
|
592
|
+
self.timing = {"stop_ms": round((time.monotonic() - started) * 1000)}
|
|
593
|
+
self.start()
|
|
594
|
+
self.restarts += 1
|
|
595
|
+
end = time.monotonic() + timeout
|
|
596
|
+
while time.monotonic() < end:
|
|
597
|
+
if ready(self.url):
|
|
598
|
+
self.timing["ready_ms"] = round((time.monotonic() - started) * 1000)
|
|
599
|
+
return True
|
|
600
|
+
time.sleep(self.READY_POLL_SECONDS)
|
|
601
|
+
raise TimeoutError(f"WDA on port {self.port} did not come back in {timeout:.0f} s")
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def plan_is_pinned(path):
|
|
605
|
+
"""Every target of a test plan has USE_IP=127.0.0.1."""
|
|
606
|
+
import plistlib
|
|
607
|
+
try:
|
|
608
|
+
with open(path, "rb") as stream:
|
|
609
|
+
plan = plistlib.load(stream)
|
|
610
|
+
except (OSError, ValueError, plistlib.InvalidFileException):
|
|
611
|
+
return False
|
|
612
|
+
targets = xctestrun_targets(plan)
|
|
613
|
+
return bool(targets) and all(
|
|
614
|
+
isinstance(target.get("EnvironmentVariables"), dict)
|
|
615
|
+
and target["EnvironmentVariables"].get("USE_IP") == LOOPBACK
|
|
616
|
+
and (not isinstance(target.get("TestingEnvironmentVariables"), dict)
|
|
617
|
+
or target["TestingEnvironmentVariables"].get("USE_IP", LOOPBACK) == LOOPBACK)
|
|
618
|
+
for target in targets)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""The Xcode Mobster runs simulators with: its version, its build and the environment to run its tools in."""
|
|
2
|
+
|
|
3
|
+
import plistlib
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import NamedTuple
|
|
7
|
+
|
|
8
|
+
from .. import mac_tools
|
|
9
|
+
from .models import SimError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Tools(NamedTuple):
|
|
13
|
+
"""Xcode's tools for one run: its version and build, and the environment to run them in."""
|
|
14
|
+
xcodebuild: str
|
|
15
|
+
git: str
|
|
16
|
+
version: str # "26.4"
|
|
17
|
+
build: str # "17E192"
|
|
18
|
+
env: dict
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def find_tools(run):
|
|
22
|
+
xcode, selected = mac_tools.developer_dir()
|
|
23
|
+
if not xcode:
|
|
24
|
+
raise SimError("environment", "Xcode isn't installed. Mobster runs simulators with it.",
|
|
25
|
+
"Install Xcode from the App Store, open it once, then run `mobster sim doctor`.")
|
|
26
|
+
# The Command Line Tools are selected: use the Xcode found anyway. Never the shell's settings for building for
|
|
27
|
+
# this Mac (mac_tools.HOST_BUILD_ENV): they break the simulator's helper build.
|
|
28
|
+
env = mac_tools.xcode_env(xcode if selected != xcode else None)
|
|
29
|
+
version, build = app_version(xcode)
|
|
30
|
+
xcodebuild = str(Path(xcode) / "usr" / "bin" / "xcodebuild")
|
|
31
|
+
if not build:
|
|
32
|
+
result = run([xcodebuild, "-version"], timeout=60, env=env)
|
|
33
|
+
version, build = parse_version(result.out)
|
|
34
|
+
if not result.ok or not build:
|
|
35
|
+
text = result.out + result.err
|
|
36
|
+
if "license" in text.lower():
|
|
37
|
+
raise SimError("environment", "Xcode's license isn't accepted yet.",
|
|
38
|
+
"Run `sudo xcodebuild -license accept` in Terminal.")
|
|
39
|
+
raise SimError("environment", f"xcodebuild didn't report its version: {result.last_line()}",
|
|
40
|
+
"Open Xcode once to finish its setup, then run `mobster sim doctor`.")
|
|
41
|
+
git = mac_tools.xcode_tool("git", developer=xcode) or ""
|
|
42
|
+
return Tools(xcodebuild=xcodebuild, git=git, version=version, build=build, env=env)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def app_version(developer_dir):
|
|
46
|
+
"""("26.4", "17E192") from the Xcode app's version.plist, else ("", "")."""
|
|
47
|
+
try:
|
|
48
|
+
with open(Path(developer_dir).parent / "version.plist", "rb") as stream:
|
|
49
|
+
info = plistlib.load(stream)
|
|
50
|
+
except (OSError, ValueError, plistlib.InvalidFileException):
|
|
51
|
+
return "", ""
|
|
52
|
+
return str(info.get("CFBundleShortVersionString") or ""), str(info.get("ProductBuildVersion") or "")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def parse_version(text):
|
|
56
|
+
"""("26.4", "17E192") from `xcodebuild -version`."""
|
|
57
|
+
version = re.search(r"Xcode\s+(\S+)", text or "")
|
|
58
|
+
build = re.search(r"Build version\s+(\S+)", text or "")
|
|
59
|
+
return (version.group(1) if version else ""), (build.group(1) if build else "")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def installed_builds():
|
|
63
|
+
"""The build versions of the Xcode apps in /Applications and ~/Applications."""
|
|
64
|
+
builds = set()
|
|
65
|
+
for root in mac_tools.XCODE_APP_DIRS:
|
|
66
|
+
try:
|
|
67
|
+
candidates = list(Path(root).glob("Xcode*.app"))
|
|
68
|
+
except OSError:
|
|
69
|
+
continue
|
|
70
|
+
for app in candidates:
|
|
71
|
+
build = app_version(app / "Contents" / "Developer")[1]
|
|
72
|
+
if build:
|
|
73
|
+
builds.add(build)
|
|
74
|
+
return builds
|