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
|
@@ -0,0 +1,978 @@
|
|
|
1
|
+
"""The USB iPhone lifecycle, driven from the setup flow."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import shutil
|
|
7
|
+
import signal
|
|
8
|
+
import subprocess
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from . import env_file, mac_tools, owner_presence, setup_errors, signing, supervised
|
|
14
|
+
from .device_info import MODEL_NAMES, enclosure_code
|
|
15
|
+
from .devices import LOOPBACK, UDID_PATTERN
|
|
16
|
+
from .mac_tools import VIDEO_PORT, WDA_PORT, XCODE_OK_TTL, XCODE_RETRY_TTL, XCODE_SHIMS
|
|
17
|
+
from .owner_presence import OwnerPresence, Poller
|
|
18
|
+
from .runner_hints import (
|
|
19
|
+
AUTOMATION_WAIT_HINT,
|
|
20
|
+
AUTOMATION_WAIT_S,
|
|
21
|
+
AUTOMATION_WAITING,
|
|
22
|
+
EXPIRED_HINT,
|
|
23
|
+
EXPOSED_HINT,
|
|
24
|
+
LOCKED_START_HINT,
|
|
25
|
+
PREARM_ASK,
|
|
26
|
+
PREARM_MIN_IOS,
|
|
27
|
+
RUNNER_HINTS,
|
|
28
|
+
build_hint,
|
|
29
|
+
runner_stamp,
|
|
30
|
+
)
|
|
31
|
+
from .supervised import BUILD_STEPS, stop_group
|
|
32
|
+
from .transport import read_kept
|
|
33
|
+
from .wda import gate as wda_gate
|
|
34
|
+
from .wda import patches as wda_patches
|
|
35
|
+
from .wda import source as wda_source
|
|
36
|
+
from .wda.patches import BUNDLE_REBRAND, LEGACY_RUNNER, RUNNER_ID
|
|
37
|
+
from .wda.watchdog import Watchdog
|
|
38
|
+
|
|
39
|
+
TEAMS_TTL = 60.0
|
|
40
|
+
USB_TTL = 5.0
|
|
41
|
+
TEAM_PATTERN = re.compile(r"[A-Z0-9]{10}")
|
|
42
|
+
WATCH_INTERVAL = 5.0
|
|
43
|
+
# WDA can wedge: /status still answers while every call that needs the phone's UI
|
|
44
|
+
# (screen reads, taps, video) hangs, e.g. with the phone locked or behind Control
|
|
45
|
+
# Center or the iOS 26 Siri glow (measured 2026-09-23: reads hung for minutes).
|
|
46
|
+
# /wda/locked is cheap when WDA is healthy, so it is the probe.
|
|
47
|
+
PROBE_INTERVAL = 3.0
|
|
48
|
+
# The watch loop looks at the probe this often; its other jobs (autostart, presence, exposure, a locked build's retry,
|
|
49
|
+
# the renewal) still run every WATCH_INTERVAL. A cause is named within engine_health.NAMED_WITHIN_S of when it begins:
|
|
50
|
+
# PROBE_TICK + PROBE_INTERVAL + a probe's time + the sampler's tick.
|
|
51
|
+
PROBE_TICK = 1.0
|
|
52
|
+
PROBE_TIMEOUT = 3.0
|
|
53
|
+
# A runner this soon after its start is still starting, not wedged (xcodebuild takes a while to answer /status).
|
|
54
|
+
STARTUP_GRACE_SECONDS = 120.0
|
|
55
|
+
# WEDGED_RESTART_SECONDS and MIN_RESTART_GAP live in wda/watchdog.py, which decides the restart.
|
|
56
|
+
LOG_TAIL = 40
|
|
57
|
+
# How long one CoreDevice lock read may take before the runner launches as it would without one.
|
|
58
|
+
HOLD_READ_TIMEOUT = 4.0
|
|
59
|
+
RUNNER_LOG_WINDOW = 16384
|
|
60
|
+
AUTOMATION_OK_KEPT = 32 # phones remembered as having answered once (prearm stops asking for them)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def wifi_runner_command(xcodebuild, plan, udid):
|
|
64
|
+
if not isinstance(udid, str) or not UDID_PATTERN.fullmatch(udid):
|
|
65
|
+
raise ValueError("A UDID is 24 to 40 hex digits and dashes")
|
|
66
|
+
return [xcodebuild, "test-without-building", "-xctestrun", str(plan), "-destination", f"id={udid}"]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# One build at a time across phones: every build rebrands and patches the shared WebDriverAgent clone, and
|
|
70
|
+
# two xcodebuilds at once would also double the heaviest job Mobster runs.
|
|
71
|
+
BUILD_LOCK = threading.Lock()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class DeviceManager:
|
|
75
|
+
"""One USB iPhone's lifecycle."""
|
|
76
|
+
|
|
77
|
+
build_step = None # the git or xcodebuild process the build is running, if any
|
|
78
|
+
|
|
79
|
+
def __init__(self, data_dir, wda_url=f"http://127.0.0.1:{WDA_PORT}", *, udid=None, root=None,
|
|
80
|
+
video_port=VIDEO_PORT):
|
|
81
|
+
self.data_dir = Path(data_dir)
|
|
82
|
+
try:
|
|
83
|
+
env_file.private_dir(self.data_dir) # keys, journal, team id and logs live here
|
|
84
|
+
except OSError:
|
|
85
|
+
pass
|
|
86
|
+
self.wda_url = wda_url.rstrip("/")
|
|
87
|
+
from urllib.parse import urlsplit
|
|
88
|
+
self.wda_port = urlsplit(self.wda_url).port or WDA_PORT
|
|
89
|
+
self.video_port = video_port
|
|
90
|
+
self.pinned = udid
|
|
91
|
+
home = Path(root) if root is not None else self.data_dir
|
|
92
|
+
self.project = self.data_dir / "WebDriverAgent"
|
|
93
|
+
self.derived = home / "wda-build"
|
|
94
|
+
self.logs = home / "logs"
|
|
95
|
+
self.settings_path = home / "device.json"
|
|
96
|
+
self.lock = threading.Lock()
|
|
97
|
+
self.build = {"state": "idle", "error": None, "log_tail": []}
|
|
98
|
+
self.runner_log_start = 0
|
|
99
|
+
self.watching = threading.Event() # set to stop watch()
|
|
100
|
+
self.health = {"responsive": None, "locked": None, "since": None, "probed_at": 0.0, "ready": None}
|
|
101
|
+
# Whether a task runs on this phone (SetupService sets it) and how a task ends when its helper is dead.
|
|
102
|
+
self.busy = lambda: False
|
|
103
|
+
self.end_task = None
|
|
104
|
+
self.idle_hooks = [] # called once when the phone has been idle for owner_presence.IDLE_RELEASE_S
|
|
105
|
+
self.runner_started_at = None
|
|
106
|
+
self.watchdog = Watchdog(self._restart_wedged_runner, url=lambda: self.wda_url, busy=lambda: self.busy(),
|
|
107
|
+
alive=self._runner_alive,
|
|
108
|
+
on_dead_runner=lambda message: self.end_task(message) if callable(self.end_task)
|
|
109
|
+
else None)
|
|
110
|
+
# Whether the phone's owner is using it (owner_presence.py); polled by watch() while no task holds the phone.
|
|
111
|
+
self.presence = OwnerPresence()
|
|
112
|
+
self.presence_poller = Poller(self.presence, self.wda_url, busy=lambda: self.busy())
|
|
113
|
+
owner_presence.register(self.wda_url, self.presence)
|
|
114
|
+
# One start or stop at a time: the watcher and the setup flow can both start the runner.
|
|
115
|
+
self.runner_lock = threading.RLock()
|
|
116
|
+
self.runner = None
|
|
117
|
+
self.relay = None
|
|
118
|
+
# What the runner started last uses: "usb" (iproxy), "wifi" (the tunnel and wireless.relay) or None (stopped).
|
|
119
|
+
# Only wireless/monitor.py starts it over Wi-Fi, with MOBSTER_WIFI_TRANSPORT on.
|
|
120
|
+
self.transport = None
|
|
121
|
+
self.wifi_address = None
|
|
122
|
+
# The last start the exposure check (check_exposure) looked at, and what answered where it must not.
|
|
123
|
+
self.exposure_checked = None
|
|
124
|
+
self.exposure = None
|
|
125
|
+
self.runner_line = self.runner_cause = None
|
|
126
|
+
self._answered = set() # phones whose helper answered (note_answered), so prearm reads no file
|
|
127
|
+
# whether the chosen phone was on the cable at the last watch round (rearm_on_attach)
|
|
128
|
+
self._attached_before = None
|
|
129
|
+
self.locked_retry_for = None # the started_at of the locked-phone build failure already tried again
|
|
130
|
+
self.cache = {}
|
|
131
|
+
# Rebuilds the runner before its signature runs out (runner_renewal.RunnerRenewal); set by SetupService.
|
|
132
|
+
self.renewal = None
|
|
133
|
+
# Phones other managers drive (devices.py): the primary never picks one as "the only phone connected".
|
|
134
|
+
self.claimed = lambda: ()
|
|
135
|
+
if udid is not None and self.settings_path.parent.exists() and self.settings().get("udid") != udid:
|
|
136
|
+
try:
|
|
137
|
+
self.save_settings(udid=udid)
|
|
138
|
+
except OSError:
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
# -- inspection ----------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
def settings(self):
|
|
144
|
+
try:
|
|
145
|
+
value = json.loads(self.settings_path.read_text())
|
|
146
|
+
return value if isinstance(value, dict) else {}
|
|
147
|
+
except (OSError, ValueError):
|
|
148
|
+
return {}
|
|
149
|
+
|
|
150
|
+
def save_settings(self, **changes):
|
|
151
|
+
settings = {**self.settings(), **changes}
|
|
152
|
+
env_file.private_dir(self.data_dir)
|
|
153
|
+
env_file.private_dir(self.settings_path.parent)
|
|
154
|
+
with env_file.open_private(self.settings_path, "w") as stream:
|
|
155
|
+
stream.write(json.dumps(settings, indent=1))
|
|
156
|
+
return settings
|
|
157
|
+
|
|
158
|
+
def tools(self):
|
|
159
|
+
"""Paths of the tools setup needs (None when missing or unusable),
|
|
160
|
+
and Xcode's state (mac_tools.xcode_status)."""
|
|
161
|
+
cached = self.cache.get("xcode")
|
|
162
|
+
if cached and time.monotonic() < cached[0]:
|
|
163
|
+
xcode = cached[1]
|
|
164
|
+
else:
|
|
165
|
+
xcode = mac_tools.xcode_status()
|
|
166
|
+
self.cache["xcode"] = (time.monotonic() + (XCODE_OK_TTL if xcode["state"] == "ok" else XCODE_RETRY_TTL),
|
|
167
|
+
xcode)
|
|
168
|
+
developer = xcode["developer_dir"]
|
|
169
|
+
return {"xcodebuild": mac_tools.xcode_tool("xcodebuild", developer) if xcode["state"] == "ok" else None,
|
|
170
|
+
"iproxy": mac_tools.tool("iproxy"), "idevice_id": mac_tools.tool("idevice_id"),
|
|
171
|
+
"ideviceinfo": mac_tools.tool("ideviceinfo"),
|
|
172
|
+
"git": mac_tools.xcode_tool("git", developer), "xcode": xcode}
|
|
173
|
+
|
|
174
|
+
def refresh(self):
|
|
175
|
+
"""Forget the cached tool and team checks, so the next read sees a fix the user just made."""
|
|
176
|
+
for key in ("xcode", "teams", "usb"):
|
|
177
|
+
self.cache.pop(key, None)
|
|
178
|
+
|
|
179
|
+
def xcode_env(self):
|
|
180
|
+
developer = self.tools()["xcode"]["developer_dir"]
|
|
181
|
+
return mac_tools.xcode_env(developer) if developer else None
|
|
182
|
+
|
|
183
|
+
def runner_env(self):
|
|
184
|
+
return {**(self.xcode_env() or mac_tools.xcode_env()), **wda_gate.runner_env("phone")}
|
|
185
|
+
|
|
186
|
+
def devices(self):
|
|
187
|
+
"""USB iPhones, with whether this Mac is trusted (lockdown info readable)."""
|
|
188
|
+
listing = mac_tools.tool("idevice_id")
|
|
189
|
+
output = mac_tools.run([listing, "-l"]) if listing else None
|
|
190
|
+
udids = []
|
|
191
|
+
for line in (output or "").splitlines():
|
|
192
|
+
udid = line.strip()
|
|
193
|
+
# 0000FE01-… entries are not physical USB phones.
|
|
194
|
+
if UDID_PATTERN.fullmatch(udid) and not udid.startswith("0000FE01") and udid not in udids:
|
|
195
|
+
udids.append(udid)
|
|
196
|
+
info_tool = mac_tools.tool("ideviceinfo")
|
|
197
|
+
devices = []
|
|
198
|
+
for udid in udids:
|
|
199
|
+
# A trusted phone's identity is stable; untrusted ones are re-read so Trust shows at once.
|
|
200
|
+
cached = self.cache.get(("info", udid))
|
|
201
|
+
info = dict(cached[1]) if cached and time.monotonic() - cached[0] < 60 else {}
|
|
202
|
+
if info_tool and not info:
|
|
203
|
+
for key, field in (("DeviceName", "name"), ("ProductType", "model"), ("ProductVersion", "ios"),
|
|
204
|
+
("DeviceEnclosureColor", "enclosure")):
|
|
205
|
+
value = mac_tools.run([info_tool, "-u", udid, "-k", key], timeout=5)
|
|
206
|
+
if value is not None:
|
|
207
|
+
info[field] = value.strip()
|
|
208
|
+
if info:
|
|
209
|
+
self.cache[("info", udid)] = (time.monotonic(), dict(info))
|
|
210
|
+
devices.append({"udid": udid, "name": info.get("name") or "iPhone", "model": info.get("model"),
|
|
211
|
+
"modelName": MODEL_NAMES.get(info.get("model")), "ios": info.get("ios"),
|
|
212
|
+
"enclosure": enclosure_code(info.get("enclosure")), "trusted": bool(info)})
|
|
213
|
+
return devices
|
|
214
|
+
|
|
215
|
+
def usb_iphones(self):
|
|
216
|
+
cached = self.cache.get("usb")
|
|
217
|
+
if cached and time.monotonic() - cached[0] < USB_TTL:
|
|
218
|
+
return cached[1]
|
|
219
|
+
count = None
|
|
220
|
+
for data_type in ("SPUSBHostDataType", "SPUSBDataType"):
|
|
221
|
+
output = mac_tools.run(["/usr/sbin/system_profiler", data_type, "-json"], timeout=8)
|
|
222
|
+
try:
|
|
223
|
+
tree = json.loads(output) if output else None
|
|
224
|
+
except ValueError:
|
|
225
|
+
tree = None
|
|
226
|
+
if isinstance(tree, dict) and isinstance(tree.get(data_type), list):
|
|
227
|
+
count = mac_tools.count_iphones(tree[data_type])
|
|
228
|
+
break
|
|
229
|
+
self.cache["usb"] = (time.monotonic(), count)
|
|
230
|
+
return count
|
|
231
|
+
|
|
232
|
+
def device(self, devices=None):
|
|
233
|
+
# never a different phone than the one chosen: a runner signed for one phone fails on another
|
|
234
|
+
devices = self.devices() if devices is None else devices
|
|
235
|
+
chosen = self.pinned or self.settings().get("udid")
|
|
236
|
+
if chosen:
|
|
237
|
+
return next((d for d in devices if d["udid"] == chosen), None)
|
|
238
|
+
claimed = set(self.claimed())
|
|
239
|
+
free = [d for d in devices if d["udid"] not in claimed]
|
|
240
|
+
return free[0] if len(free) == 1 else None
|
|
241
|
+
|
|
242
|
+
def choose(self, udid):
|
|
243
|
+
if not isinstance(udid, str) or not UDID_PATTERN.fullmatch(udid):
|
|
244
|
+
raise ValueError("Choose a connected iPhone")
|
|
245
|
+
if self.pinned and udid != self.pinned:
|
|
246
|
+
raise LookupError("This device is one iPhone; set up another iPhone as its own device")
|
|
247
|
+
if udid in set(self.claimed()):
|
|
248
|
+
raise LookupError("That iPhone is already set up as another device. Choose a different iPhone.")
|
|
249
|
+
if not any(d["udid"] == udid for d in self.devices()):
|
|
250
|
+
raise LookupError("That iPhone is not connected")
|
|
251
|
+
name = next(d["name"] for d in self.devices() if d["udid"] == udid)
|
|
252
|
+
if udid != self.settings().get("udid"):
|
|
253
|
+
self.stop_runner()
|
|
254
|
+
self.save_settings(udid=udid, device_name=name)
|
|
255
|
+
|
|
256
|
+
def teams(self):
|
|
257
|
+
if "teams" in self.cache and time.monotonic() - self.cache["teams"][0] < TEAMS_TTL:
|
|
258
|
+
return self.cache["teams"][1]
|
|
259
|
+
teams = signing.merge_teams(signing.account_teams(), signing.certificate_teams())
|
|
260
|
+
self.cache["teams"] = (time.monotonic(), teams)
|
|
261
|
+
return teams
|
|
262
|
+
|
|
263
|
+
def driver(self):
|
|
264
|
+
"""What installs and launches the runner for this phone: "xcodebuild" (Xcode signs and runs it), or, when it was
|
|
265
|
+
signed on this Mac with the team's profile (team_signing), "pymobiledevice3" or "go-ios"."""
|
|
266
|
+
return self.settings().get("driver") or "xcodebuild"
|
|
267
|
+
|
|
268
|
+
def built(self):
|
|
269
|
+
"""Built for the chosen phone (a build records the phone it was made for), and still signed."""
|
|
270
|
+
settings = self.settings()
|
|
271
|
+
built_for = settings.get("built_for", settings.get("udid"))
|
|
272
|
+
if self.driver() != "xcodebuild":
|
|
273
|
+
runner = (settings.get("signed_runner") or {}).get("path")
|
|
274
|
+
return bool(built_for) and built_for == settings.get("udid") and bool(runner) and Path(runner).is_dir() \
|
|
275
|
+
and not self.expired() and self.patched()
|
|
276
|
+
return bool(built_for) and built_for == settings.get("udid") and any(
|
|
277
|
+
self.derived.glob("Build/Products/*.xctestrun")) and not self.expired() and self.patched()
|
|
278
|
+
|
|
279
|
+
def patched(self):
|
|
280
|
+
"""Whether the build was made with the WDA patches this Mobster has (wda_patches.patch_digest),
|
|
281
|
+
not an older set."""
|
|
282
|
+
return self.settings().get("built_patches") == wda_patches.patch_digest()
|
|
283
|
+
|
|
284
|
+
def out_of_date(self):
|
|
285
|
+
"""A build is there, made with other WDA patches than this Mobster has: it has to be built again."""
|
|
286
|
+
return any(self.derived.glob("Build/Products/*.xctestrun")) and not self.patched()
|
|
287
|
+
|
|
288
|
+
def signature_expiry(self):
|
|
289
|
+
settings = self.settings()
|
|
290
|
+
if "expires_at" not in settings and any(self.derived.glob(signing.PROFILE_GLOB)):
|
|
291
|
+
settings = self.save_settings(expires_at=signing.profile_expiry(self.derived))
|
|
292
|
+
value = settings.get("expires_at")
|
|
293
|
+
return value if isinstance(value, (int, float)) and not isinstance(value, bool) else None
|
|
294
|
+
|
|
295
|
+
def expired(self, now=None):
|
|
296
|
+
expires = self.signature_expiry()
|
|
297
|
+
return expires is not None and (time.time() if now is None else now) >= expires
|
|
298
|
+
|
|
299
|
+
def _wda_get(self, path, timeout):
|
|
300
|
+
# one kept connection: a connection per read ran WDA out of descriptors
|
|
301
|
+
return read_kept(self.wda_url, path, timeout)
|
|
302
|
+
|
|
303
|
+
def wda_ready(self, timeout=1.5):
|
|
304
|
+
try:
|
|
305
|
+
return (self._wda_get("/status", timeout).get("value") or {}).get("ready") is True
|
|
306
|
+
except Exception:
|
|
307
|
+
return False
|
|
308
|
+
|
|
309
|
+
def _runner_alive(self):
|
|
310
|
+
"""Whether the runner's child process runs now (not just its supervisor thread)."""
|
|
311
|
+
runner = self.runner
|
|
312
|
+
alive = getattr(runner, "alive", None)
|
|
313
|
+
return bool(alive) if alive is not None else bool(runner is not None and runner.running)
|
|
314
|
+
|
|
315
|
+
def runner_waiting(self):
|
|
316
|
+
""""unlock" while the runner is held back because the phone is locked (``_runner_hold``), else None."""
|
|
317
|
+
runner = self.runner
|
|
318
|
+
return "unlock" if getattr(runner, "held", None) == "locked" and not self._runner_alive() else None
|
|
319
|
+
|
|
320
|
+
def _runner_hold(self):
|
|
321
|
+
# xcodebuild on a locked phone waits 60 s and fails, and each failure counts toward 'didn't start'
|
|
322
|
+
from .wireless import devicectl
|
|
323
|
+
udid = self.pinned or self.settings().get("udid")
|
|
324
|
+
return "locked" if udid and devicectl.read_lock_state(udid, timeout=HOLD_READ_TIMEOUT) is True else None
|
|
325
|
+
|
|
326
|
+
def retry_locked_build(self):
|
|
327
|
+
build = self.build
|
|
328
|
+
problem = build.get("problem") or {}
|
|
329
|
+
key = build.get("started_at")
|
|
330
|
+
if build.get("state") != "failed" or problem.get("kind") != "locked" or key is None \
|
|
331
|
+
or key == self.locked_retry_for:
|
|
332
|
+
return False
|
|
333
|
+
settings = self.settings()
|
|
334
|
+
udid, team = self.pinned or settings.get("udid"), build.get("team") or settings.get("team")
|
|
335
|
+
if not udid or not team or self.device() is None:
|
|
336
|
+
return False
|
|
337
|
+
from .wireless import devicectl
|
|
338
|
+
if devicectl.read_lock_state(udid, timeout=HOLD_READ_TIMEOUT) is not False:
|
|
339
|
+
return False
|
|
340
|
+
self.locked_retry_for = key
|
|
341
|
+
self.start_build(team)
|
|
342
|
+
return True
|
|
343
|
+
|
|
344
|
+
def _relaunch_gate(self):
|
|
345
|
+
# the watchdog admits no relaunch while an action is in flight or an outcome under a task is unknown
|
|
346
|
+
if self.watching.is_set():
|
|
347
|
+
return None
|
|
348
|
+
if self.watchdog.admit(time.monotonic()) is not None:
|
|
349
|
+
return None
|
|
350
|
+
return lambda: self.watchdog.done(True)
|
|
351
|
+
|
|
352
|
+
def wda_alive(self, attempts=3, timeout=4.0, gap=2.0):
|
|
353
|
+
# WDA serves one request at a time, so a busy runner can miss one /status; a new one would replace it mid-task
|
|
354
|
+
for attempt in range(attempts):
|
|
355
|
+
if self.wda_ready(timeout=timeout):
|
|
356
|
+
return True
|
|
357
|
+
if attempt + 1 < attempts:
|
|
358
|
+
time.sleep(gap)
|
|
359
|
+
return False
|
|
360
|
+
|
|
361
|
+
def attached(self, udid=None):
|
|
362
|
+
"""Whether the chosen (or ``udid``'s) iPhone is plugged in now; None when that can't be told."""
|
|
363
|
+
return mac_tools.usb_attached(udid or self.pinned or self.settings().get("udid"))
|
|
364
|
+
|
|
365
|
+
def locked(self):
|
|
366
|
+
"""WDA answers but the phone is locked (the last probe)."""
|
|
367
|
+
return self.health["locked"] is True
|
|
368
|
+
|
|
369
|
+
def health_error(self):
|
|
370
|
+
"""What to tell the user when WDA answers but cannot reach the phone's UI."""
|
|
371
|
+
if self.locked():
|
|
372
|
+
return "Your iPhone is locked. Unlock it to run tasks and see its screen."
|
|
373
|
+
if self.health["responsive"] is False:
|
|
374
|
+
return "Your iPhone isn't responding. Unlock it and close Control Center or Siri if they're open."
|
|
375
|
+
return None
|
|
376
|
+
|
|
377
|
+
def prearm(self, udid=None, ios=None):
|
|
378
|
+
udid = udid or self.pinned or self.settings().get("udid")
|
|
379
|
+
if not udid or udid in self._answered or udid in (self.settings().get("automation_ok") or []):
|
|
380
|
+
return None
|
|
381
|
+
if ios is None: # from the phone list's cache (devices()): asked every second, it never runs a tool itself
|
|
382
|
+
cached = self.cache.get(("info", udid))
|
|
383
|
+
ios = cached[1].get("ios") if cached else None
|
|
384
|
+
try:
|
|
385
|
+
major = int(str(ios).split(".")[0])
|
|
386
|
+
except (TypeError, ValueError):
|
|
387
|
+
return None
|
|
388
|
+
return "ui_automation" if major >= PREARM_MIN_IOS else None
|
|
389
|
+
|
|
390
|
+
def note_answered(self):
|
|
391
|
+
udid = self.pinned or self.settings().get("udid")
|
|
392
|
+
if not udid or udid in self._answered:
|
|
393
|
+
return
|
|
394
|
+
kept = list(self.settings().get("automation_ok") or [])
|
|
395
|
+
if udid not in kept:
|
|
396
|
+
self.save_settings(automation_ok=(kept + [udid])[-AUTOMATION_OK_KEPT:])
|
|
397
|
+
self._answered.add(udid)
|
|
398
|
+
|
|
399
|
+
def runner_state(self, ready=None):
|
|
400
|
+
if ready is None:
|
|
401
|
+
ready = self.wda_ready()
|
|
402
|
+
if ready:
|
|
403
|
+
self.note_answered()
|
|
404
|
+
state = self._runner_state(ready)
|
|
405
|
+
if state["state"] != "running" and (asked := self.prearm()):
|
|
406
|
+
state["prearm"] = {"kind": asked, "ask": PREARM_ASK}
|
|
407
|
+
return state
|
|
408
|
+
|
|
409
|
+
def _runner_state(self, ready):
|
|
410
|
+
if self.runner is None or not self.runner.running:
|
|
411
|
+
# A runner started outside Mobster (scripts/usb-wda.sh, Xcode) serves just as well.
|
|
412
|
+
if ready:
|
|
413
|
+
return {"state": "running", "error": self.health_error(), "external": True}
|
|
414
|
+
if not self.exposure and self.settings().get("autostart") and self.built():
|
|
415
|
+
return {"state": "starting", "error": None} # the watch loop starts it within WATCH_INTERVAL
|
|
416
|
+
return {"state": "stopped", "error": EXPOSED_HINT if self.exposure else None}
|
|
417
|
+
if ready:
|
|
418
|
+
return {"state": "running", "error": self.health_error()}
|
|
419
|
+
if self.runner_waiting() == "unlock":
|
|
420
|
+
return {"state": "starting", "error": LOCKED_START_HINT, "waiting": "unlock"}
|
|
421
|
+
failing = self.runner.restarts >= 3 and self.runner.last_exit not in (None, 0)
|
|
422
|
+
hint = self.runner_hint()
|
|
423
|
+
return {"state": "failed" if failing else "starting",
|
|
424
|
+
"error": hint or ("Mobster's helper keeps stopping. Unlock your iPhone and check Developer Mode."
|
|
425
|
+
if failing else None)}
|
|
426
|
+
|
|
427
|
+
def runner_hint(self, now=None):
|
|
428
|
+
# An expired profile fails with the same "invalid code signature ... not explicitly trusted"
|
|
429
|
+
# sentence as an untrusted one, so the log alone would give the wrong advice.
|
|
430
|
+
self.runner_cause = None
|
|
431
|
+
if self.expired():
|
|
432
|
+
return EXPIRED_HINT
|
|
433
|
+
try:
|
|
434
|
+
with open(self.logs / "wda-runner.log", "rb") as log:
|
|
435
|
+
size = log.seek(0, os.SEEK_END)
|
|
436
|
+
log.seek(max(self.runner_log_start, size - RUNNER_LOG_WINDOW))
|
|
437
|
+
text = log.read().decode("utf-8", "replace")
|
|
438
|
+
except OSError:
|
|
439
|
+
return None
|
|
440
|
+
# The newest matching line wins: an unlock prompt followed by success is stale.
|
|
441
|
+
latest = None
|
|
442
|
+
self.runner_line = self.runner_cause = None
|
|
443
|
+
waiting_since = None # when the runner said "Running tests..." with nothing after it yet
|
|
444
|
+
for line in text.splitlines():
|
|
445
|
+
if "Running tests..." in line:
|
|
446
|
+
waiting_since = runner_stamp(line)
|
|
447
|
+
elif "Test Suite " in line or "Failed to initialize" in line:
|
|
448
|
+
waiting_since = None
|
|
449
|
+
for needle, cause, hint in RUNNER_HINTS:
|
|
450
|
+
if needle in line:
|
|
451
|
+
latest, self.runner_line, self.runner_cause = hint, line, cause
|
|
452
|
+
break
|
|
453
|
+
if "ServerURLHere" in line:
|
|
454
|
+
latest = self.runner_line = self.runner_cause = waiting_since = None
|
|
455
|
+
if waiting_since is not None and (time.time() if now is None else now) - waiting_since >= AUTOMATION_WAIT_S:
|
|
456
|
+
latest, self.runner_line, self.runner_cause = AUTOMATION_WAIT_HINT, AUTOMATION_WAITING, "ui_automation_off"
|
|
457
|
+
return latest
|
|
458
|
+
|
|
459
|
+
def runner_announced(self):
|
|
460
|
+
# unlike /status, which the runner before it may still answer for a few seconds after a restart
|
|
461
|
+
try:
|
|
462
|
+
with open(self.logs / "wda-runner.log", "rb") as log:
|
|
463
|
+
size = log.seek(0, os.SEEK_END)
|
|
464
|
+
log.seek(max(self.runner_log_start, size - 4 * RUNNER_LOG_WINDOW))
|
|
465
|
+
return b"ServerURLHere" in log.read()
|
|
466
|
+
except OSError:
|
|
467
|
+
return False
|
|
468
|
+
|
|
469
|
+
def runner_problem(self):
|
|
470
|
+
"""The runner's current failure as a one-line fix (setup_errors), from the line runner_hint matched."""
|
|
471
|
+
if self.expired():
|
|
472
|
+
return {"kind": "expired", "fix": "Refresh Mobster's helper", "action": "renew", "raw": EXPIRED_HINT}
|
|
473
|
+
if self.runner_waiting() == "unlock":
|
|
474
|
+
# Nothing to press: it starts by itself once the phone is unlocked.
|
|
475
|
+
return {"kind": "locked", "fix": LOCKED_START_HINT, "action": None, "raw": LOCKED_START_HINT}
|
|
476
|
+
line = getattr(self, "runner_line", None)
|
|
477
|
+
return setup_errors.translate(line) if line else None
|
|
478
|
+
|
|
479
|
+
# -- actions ---------------------------------------------------------------
|
|
480
|
+
|
|
481
|
+
def start_build(self, team=None, renewal=False):
|
|
482
|
+
if team is None:
|
|
483
|
+
# the saved team signs only while Xcode still lists it: a stale team never signs a build nobody picked
|
|
484
|
+
team = self.settings().get("team")
|
|
485
|
+
if not TEAM_PATTERN.fullmatch(team or "") or team not in {known["id"] for known in self.teams()}:
|
|
486
|
+
raise ValueError("Choose your Apple Account")
|
|
487
|
+
if not TEAM_PATTERN.fullmatch(team or ""):
|
|
488
|
+
raise ValueError("A team id is 10 letters and digits")
|
|
489
|
+
device = self.device()
|
|
490
|
+
if device is None:
|
|
491
|
+
raise LookupError("Connect and choose your iPhone first")
|
|
492
|
+
tools = self.tools()
|
|
493
|
+
if tools["xcode"]["state"] != "ok" or not tools["git"]:
|
|
494
|
+
raise LookupError("Finish installing Xcode first (Setup › Get your Mac ready)")
|
|
495
|
+
with self.lock:
|
|
496
|
+
if self.build["state"] == "running":
|
|
497
|
+
return
|
|
498
|
+
# The first build also fetches WebDriverAgent and prepares the phone, so it takes longest.
|
|
499
|
+
self.build = {"state": "running", "error": None, "log_tail": [], "started_at": time.time(),
|
|
500
|
+
"first": not any(self.derived.glob("Build/Products/*.xctestrun")), "renewal": renewal,
|
|
501
|
+
"team": team}
|
|
502
|
+
self.save_settings(udid=device["udid"], device_name=device["name"])
|
|
503
|
+
threading.Thread(target=self._build, args=(team, device["udid"]), name="mobster-wda-build",
|
|
504
|
+
daemon=True).start()
|
|
505
|
+
|
|
506
|
+
def _build(self, team, udid):
|
|
507
|
+
log_path = self.logs / "wda-build.log"
|
|
508
|
+
env_file.private_dir(self.logs)
|
|
509
|
+
tail = []
|
|
510
|
+
if not BUILD_LOCK.acquire(blocking=False):
|
|
511
|
+
with self.lock:
|
|
512
|
+
self.build["log_tail"] = ["Waiting for another iPhone's build to finish."]
|
|
513
|
+
BUILD_LOCK.acquire()
|
|
514
|
+
try:
|
|
515
|
+
self._build_locked(team, udid, log_path, tail)
|
|
516
|
+
finally:
|
|
517
|
+
BUILD_LOCK.release()
|
|
518
|
+
|
|
519
|
+
def _build_locked(self, team, udid, log_path, tail):
|
|
520
|
+
try:
|
|
521
|
+
with env_file.open_private(log_path, "w") as log:
|
|
522
|
+
git = mac_tools.tool("git") or "git"
|
|
523
|
+
if self.project.exists() and not wda_source.is_pinned(git, self.project):
|
|
524
|
+
shutil.rmtree(self.project) # an older or unpinned checkout: fetch the pinned release
|
|
525
|
+
if not self.project.exists():
|
|
526
|
+
self._step(wda_source.clone_command(git, self.project), log, tail)
|
|
527
|
+
wda_source.verify(git, self.project)
|
|
528
|
+
log.write(f"WebDriverAgent {wda_source.REF} ({wda_source.COMMIT})\n")
|
|
529
|
+
self._rebrand(team)
|
|
530
|
+
self._drop_stale_profiles(team, log)
|
|
531
|
+
try:
|
|
532
|
+
wda_patches.patch_wda(self.project)
|
|
533
|
+
except RuntimeError as error:
|
|
534
|
+
tail.append(f"error: {error}") # shown as the build's error
|
|
535
|
+
log.write(tail[-1] + "\n")
|
|
536
|
+
raise
|
|
537
|
+
# USE_IP goes into the built test plan: WDA then listens on the phone's loopback,
|
|
538
|
+
# which USB (usbmux) reaches, and not on its Wi-Fi address.
|
|
539
|
+
self._step([mac_tools.tool("xcodebuild"), "-project", str(self.project / "WebDriverAgent.xcodeproj"),
|
|
540
|
+
"-scheme", "WebDriverAgentRunner", "-destination", f"id={udid}",
|
|
541
|
+
"-allowProvisioningUpdates", "-derivedDataPath", str(self.derived),
|
|
542
|
+
f"DEVELOPMENT_TEAM={team}", f"USE_IP={LOOPBACK}", "build-for-testing"], log, tail)
|
|
543
|
+
self._remove_legacy_runner(team, udid, log)
|
|
544
|
+
# autostart: the watch loop starts the new helper (autostart()), whichever window is open or none. Before,
|
|
545
|
+
# only the Mac app's setup watcher started it, and it isn't mounted during the first run: after the first
|
|
546
|
+
# install the helper stayed stopped with "Starting Mobster's helper" on screen (17 Oct).
|
|
547
|
+
self.save_settings(team=team, built_for=udid, built_patches=wda_patches.patch_digest(),
|
|
548
|
+
expires_at=signing.profile_expiry(self.derived), autostart=True)
|
|
549
|
+
with self.lock:
|
|
550
|
+
self.build = {**self.build, "state": "succeeded", "error": None, "problem": None,
|
|
551
|
+
"log_tail": tail[-LOG_TAIL:]}
|
|
552
|
+
except Exception as error:
|
|
553
|
+
text = "\n".join(tail) if tail else str(error)
|
|
554
|
+
with self.lock:
|
|
555
|
+
self.build = {**self.build, "state": "failed", "error": build_hint(tail) if tail else str(error)[:300],
|
|
556
|
+
"problem": self._team_problem(setup_errors.problem(text), team),
|
|
557
|
+
"log_tail": tail[-LOG_TAIL:]}
|
|
558
|
+
|
|
559
|
+
def _team_problem(self, problem, team):
|
|
560
|
+
if not problem or problem.get("kind") != "xcode_signin" \
|
|
561
|
+
or not re.search(r"no account for team", problem.get("raw") or "", re.I):
|
|
562
|
+
return problem
|
|
563
|
+
try:
|
|
564
|
+
known = {item["id"] for item in self.teams()}
|
|
565
|
+
except Exception: # noqa: BLE001 -- unreadable: keep the sign-in advice
|
|
566
|
+
return problem
|
|
567
|
+
if not known or team in known:
|
|
568
|
+
return problem
|
|
569
|
+
return {**problem, "kind": "team_account", "action": "choose_team",
|
|
570
|
+
"fix": f"Xcode has no Apple Account for team {team}. Choose another team"}
|
|
571
|
+
|
|
572
|
+
def _step(self, command, log, tail):
|
|
573
|
+
# git and xcodebuild come from Xcode, never the /usr/bin stubs, and run with its developer directory.
|
|
574
|
+
if command[0] in XCODE_SHIMS:
|
|
575
|
+
command = [mac_tools.tool(command[0]) or command[0], *command[1:]]
|
|
576
|
+
name = Path(command[0]).name
|
|
577
|
+
if self.watching.is_set():
|
|
578
|
+
raise RuntimeError(f"Mobster is closing: {name} did not start")
|
|
579
|
+
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
|
|
580
|
+
stdin=subprocess.DEVNULL, start_new_session=True, env=self.xcode_env())
|
|
581
|
+
BUILD_STEPS.add(process)
|
|
582
|
+
self.build_step = process
|
|
583
|
+
started = time.monotonic()
|
|
584
|
+
timer = threading.Timer(supervised.BUILD_STEP_LIMIT, stop_group, (process,))
|
|
585
|
+
timer.daemon = True
|
|
586
|
+
timer.start()
|
|
587
|
+
try:
|
|
588
|
+
for line in process.stdout:
|
|
589
|
+
log.write(line)
|
|
590
|
+
tail.append(line.rstrip()[:400])
|
|
591
|
+
del tail[:-LOG_TAIL * 5]
|
|
592
|
+
with self.lock:
|
|
593
|
+
self.build["log_tail"] = tail[-LOG_TAIL:]
|
|
594
|
+
code = process.wait()
|
|
595
|
+
finally:
|
|
596
|
+
timer.cancel()
|
|
597
|
+
process.stdout.close()
|
|
598
|
+
BUILD_STEPS.discard(process)
|
|
599
|
+
self.build_step = None
|
|
600
|
+
if code != 0 and time.monotonic() - started >= supervised.BUILD_STEP_LIMIT:
|
|
601
|
+
raise RuntimeError(f"{name} ran for {supervised.BUILD_STEP_LIMIT // 60} minutes and was stopped")
|
|
602
|
+
if code != 0:
|
|
603
|
+
raise RuntimeError(f"{name} exited with {code}")
|
|
604
|
+
|
|
605
|
+
def _rebrand(self, team=None):
|
|
606
|
+
"""Mobster's bundle ids in the clone; the runner's carries the team's suffix (see BUNDLE_REBRAND)."""
|
|
607
|
+
project = self.project / "WebDriverAgent.xcodeproj" / "project.pbxproj"
|
|
608
|
+
text = project.read_text()
|
|
609
|
+
for old, new in BUNDLE_REBRAND.items():
|
|
610
|
+
if old != "com.facebook.WebDriverAgentRunner":
|
|
611
|
+
text = text.replace(old, new)
|
|
612
|
+
text = RUNNER_ID.sub(signing.runner_bundle_id(team), text)
|
|
613
|
+
project.write_text(text)
|
|
614
|
+
|
|
615
|
+
def _remove_legacy_runner(self, team, udid, log):
|
|
616
|
+
# the plain-id runner earlier versions installed is a second icon and takes one of a free Apple ID's 3 app slots
|
|
617
|
+
if signing.runner_bundle_id(team) == signing.RUNNER_BUNDLE or udid in self.settings().get("legacy_removed", []):
|
|
618
|
+
return
|
|
619
|
+
xcrun = mac_tools.tool("xcrun")
|
|
620
|
+
if not xcrun:
|
|
621
|
+
return
|
|
622
|
+
try:
|
|
623
|
+
result = subprocess.run([xcrun, "devicectl", "device", "uninstall", "app", "--device", udid,
|
|
624
|
+
LEGACY_RUNNER], capture_output=True, text=True, timeout=90, env=self.xcode_env())
|
|
625
|
+
except (OSError, subprocess.SubprocessError) as error:
|
|
626
|
+
log.write(f"Could not remove the old runner ({LEGACY_RUNNER}): {error}\n")
|
|
627
|
+
return
|
|
628
|
+
output = (result.stdout + result.stderr).strip()
|
|
629
|
+
if result.returncode == 0:
|
|
630
|
+
log.write(f"Removed the old runner ({LEGACY_RUNNER}) from the iPhone.\n")
|
|
631
|
+
elif re.search(r"not (installed|found)|no such app|couldn.t find|does not exist", output, re.I):
|
|
632
|
+
log.write(f"The old runner ({LEGACY_RUNNER}) isn't on the iPhone.\n")
|
|
633
|
+
else:
|
|
634
|
+
log.write(f"Could not remove the old runner ({LEGACY_RUNNER}): {output[-300:]}\n")
|
|
635
|
+
return
|
|
636
|
+
self.save_settings(legacy_removed=[*self.settings().get("legacy_removed", []), udid][-8:])
|
|
637
|
+
|
|
638
|
+
def _drop_stale_profiles(self, team, log):
|
|
639
|
+
# otherwise -allowProvisioningUpdates signs with the cached, expiring profile again
|
|
640
|
+
window = signing.renew_window()
|
|
641
|
+
expires = self.signature_expiry()
|
|
642
|
+
if expires is None or expires - time.time() > window:
|
|
643
|
+
return
|
|
644
|
+
for path in signing.stale_runner_profiles(team, time.time() + window):
|
|
645
|
+
try:
|
|
646
|
+
path.unlink()
|
|
647
|
+
log.write(f"Removed an expiring runner profile: {path.name}\n")
|
|
648
|
+
except OSError:
|
|
649
|
+
pass
|
|
650
|
+
|
|
651
|
+
def start_runner(self):
|
|
652
|
+
with self.runner_lock:
|
|
653
|
+
if self.transport == "wifi" and self.wifi_address and self.attached() is not True:
|
|
654
|
+
# A runner on Wi-Fi restarts on Wi-Fi (Setup's Start, a wedged runner), while the phone stays unplugged.
|
|
655
|
+
self._start_wifi_runner(self.wifi_address)
|
|
656
|
+
return
|
|
657
|
+
self._start_runner()
|
|
658
|
+
|
|
659
|
+
def _start_runner(self):
|
|
660
|
+
# Every start (Setup's Start, autostart, probe()'s restart, a renewal's restart) runs here under
|
|
661
|
+
# runner_lock, and close() sets ``watching`` before it takes that lock to stop the runner: a start
|
|
662
|
+
# that gets here afterwards would undo that stop.
|
|
663
|
+
if self.watching.is_set():
|
|
664
|
+
raise LookupError("Mobster is quitting")
|
|
665
|
+
settings = self.settings()
|
|
666
|
+
device = self.device()
|
|
667
|
+
if device is None:
|
|
668
|
+
raise LookupError("Connect your iPhone first")
|
|
669
|
+
if self.expired():
|
|
670
|
+
raise LookupError(EXPIRED_HINT)
|
|
671
|
+
if not self.built() or not settings.get("team"):
|
|
672
|
+
raise LookupError("Build the runner first")
|
|
673
|
+
udid = device["udid"]
|
|
674
|
+
relay = mac_tools.relay_command(udid, self.wda_port, self.video_port)
|
|
675
|
+
self.stop_runner()
|
|
676
|
+
# Our own relay is stopped: a listener still on these ports is another Mobster's (DR-68). Never take it over,
|
|
677
|
+
# and never drive whatever phone answers there.
|
|
678
|
+
from .devices import listening
|
|
679
|
+
if listening(self.wda_port) or listening(self.video_port):
|
|
680
|
+
raise LookupError(f"Another Mobster is using port {self.wda_port} or {self.video_port} on this Mac, "
|
|
681
|
+
"so this iPhone's helper didn't start. Quit the other Mobster, then try again.")
|
|
682
|
+
wda_patches.pin_loopback(self.derived / "Build" / "Products")
|
|
683
|
+
try:
|
|
684
|
+
self.runner_log_start = (self.logs / "wda-runner.log").stat().st_size
|
|
685
|
+
except OSError:
|
|
686
|
+
self.runner_log_start = 0
|
|
687
|
+
# A new runner's health is unknown. ``watching`` and ``runner_lock`` stay the ones __init__ made: the
|
|
688
|
+
# watch loop waits on that Event (close() sets it) and a second start waits on that lock.
|
|
689
|
+
self.health = {"responsive": None, "locked": None, "since": None, "probed_at": 0.0, "ready": None}
|
|
690
|
+
if self.driver() == "xcodebuild":
|
|
691
|
+
command, env = [
|
|
692
|
+
mac_tools.tool("xcodebuild"), "-project", str(self.project / "WebDriverAgent.xcodeproj"),
|
|
693
|
+
"-scheme", "WebDriverAgentRunner", "-destination", f"id={udid}",
|
|
694
|
+
"-derivedDataPath", str(self.derived), f"DEVELOPMENT_TEAM={settings['team']}",
|
|
695
|
+
f"USE_IP={LOOPBACK}", "test-without-building"], self.runner_env()
|
|
696
|
+
else:
|
|
697
|
+
command, env = self._signed_runner_command(udid, settings)
|
|
698
|
+
self.runner = supervised.Supervised("wda-runner", command, self.logs / "wda-runner.log", env=env,
|
|
699
|
+
gate=self._relaunch_gate, hold=self._runner_hold)
|
|
700
|
+
self.relay = supervised.Supervised("usb-relay", relay, self.logs / "usb-relay.log", gate=self._relaunch_gate)
|
|
701
|
+
self.runner_started_at = time.monotonic()
|
|
702
|
+
self.relay.start()
|
|
703
|
+
self.runner.start()
|
|
704
|
+
self.transport = "usb"
|
|
705
|
+
self.save_settings(autostart=True)
|
|
706
|
+
|
|
707
|
+
def _signed_runner_command(self, udid, settings):
|
|
708
|
+
"""The launch of a runner signed on this Mac (team_signing), with the environment it runs in: no Xcode's."""
|
|
709
|
+
from .team_signing import tooling
|
|
710
|
+
bundle_id = (settings.get("signed_runner") or {}).get("bundle_id")
|
|
711
|
+
if not bundle_id:
|
|
712
|
+
raise LookupError("Set the helper up again (Setup › Install Mobster's helper)")
|
|
713
|
+
env = tooling.runner_env("phone")
|
|
714
|
+
if self.driver() == "go-ios":
|
|
715
|
+
program = tooling.go_ios()
|
|
716
|
+
command = tooling.go_ios_launch_command(program, udid, bundle_id, env) if program else None
|
|
717
|
+
else:
|
|
718
|
+
program = tooling.pmd3()
|
|
719
|
+
command = tooling.launch_command(program, udid, bundle_id, env) if program else None
|
|
720
|
+
if command is None:
|
|
721
|
+
raise LookupError(f"{self.driver()} isn't on this Mac, so Mobster's helper can't start")
|
|
722
|
+
return command, {**os.environ, "NO_COLOR": "1", "TERM": "dumb", "COLUMNS": "200"}
|
|
723
|
+
|
|
724
|
+
def start_wifi_runner(self, address, peer_udid):
|
|
725
|
+
with self.runner_lock:
|
|
726
|
+
if str(peer_udid or "").upper() != str(self.pinned or self.settings().get("udid") or "").upper():
|
|
727
|
+
raise LookupError("CoreDevice reported a different iPhone for that Wi-Fi address, so Mobster didn't "
|
|
728
|
+
"start on it")
|
|
729
|
+
self._start_wifi_runner(address)
|
|
730
|
+
|
|
731
|
+
def wifi_udid(self):
|
|
732
|
+
"""This device's UDID while its runner runs over Wi-Fi, else None."""
|
|
733
|
+
running = self.runner is not None and self.runner.running
|
|
734
|
+
return (self.pinned or self.settings().get("udid")) if running and self.transport == "wifi" else None
|
|
735
|
+
|
|
736
|
+
def _start_wifi_runner(self, address):
|
|
737
|
+
from .wireless import relay as wifi_relay
|
|
738
|
+
from .wireless import xctestrun
|
|
739
|
+
if self.watching.is_set():
|
|
740
|
+
raise LookupError("Mobster is quitting")
|
|
741
|
+
settings = self.settings()
|
|
742
|
+
udid = self.pinned or settings.get("udid")
|
|
743
|
+
if not udid:
|
|
744
|
+
raise LookupError("Choose your iPhone in Setup first")
|
|
745
|
+
if self.expired():
|
|
746
|
+
raise LookupError(EXPIRED_HINT)
|
|
747
|
+
if not self.built() or not settings.get("team"):
|
|
748
|
+
raise LookupError("Build the runner first")
|
|
749
|
+
xcodebuild = mac_tools.tool("xcodebuild")
|
|
750
|
+
if not xcodebuild:
|
|
751
|
+
raise LookupError("Finish installing Xcode first (Setup › Get your Mac ready)")
|
|
752
|
+
try:
|
|
753
|
+
# A tunnel address (fd00::/8) that this Mac reaches through the CoreDevice tunnel, never through Wi-Fi.
|
|
754
|
+
target = wifi_relay.require_tunnel(address)
|
|
755
|
+
plan = xctestrun.write(self.derived / "Build" / "Products", self.derived / xctestrun.FOLDER, target)
|
|
756
|
+
except ValueError as error:
|
|
757
|
+
raise LookupError(str(error)) from None
|
|
758
|
+
self._stop_runner(False)
|
|
759
|
+
relay = wifi_relay.Relay(target, [(self.wda_port, WDA_PORT), (self.video_port, VIDEO_PORT)])
|
|
760
|
+
try:
|
|
761
|
+
relay.start()
|
|
762
|
+
except OSError:
|
|
763
|
+
raise LookupError(f"Another program is using port {self.wda_port} or "
|
|
764
|
+
f"{self.video_port} on this Mac") from None
|
|
765
|
+
try:
|
|
766
|
+
self.runner_log_start = (self.logs / "wda-runner.log").stat().st_size
|
|
767
|
+
except OSError:
|
|
768
|
+
self.runner_log_start = 0
|
|
769
|
+
self.health = {"responsive": None, "locked": None, "since": None, "probed_at": 0.0, "ready": None}
|
|
770
|
+
self.relay = relay
|
|
771
|
+
self.runner = supervised.Supervised("wda-runner", wifi_runner_command(xcodebuild, plan, udid),
|
|
772
|
+
self.logs / "wda-runner.log", env=self.runner_env(),
|
|
773
|
+
gate=self._relaunch_gate, hold=self._runner_hold)
|
|
774
|
+
self.runner_started_at = time.monotonic()
|
|
775
|
+
self.runner.start()
|
|
776
|
+
self.transport, self.wifi_address = "wifi", target
|
|
777
|
+
|
|
778
|
+
def stop_runner(self, remember=False):
|
|
779
|
+
with self.runner_lock:
|
|
780
|
+
self._stop_runner(remember)
|
|
781
|
+
|
|
782
|
+
def check_exposure(self, hostnames=None):
|
|
783
|
+
with self.runner_lock:
|
|
784
|
+
runner = self.runner
|
|
785
|
+
if runner is None or not runner.running or self.exposure_checked == self.runner_started_at:
|
|
786
|
+
return
|
|
787
|
+
if not self.wda_ready():
|
|
788
|
+
return
|
|
789
|
+
self.exposure_checked = self.runner_started_at
|
|
790
|
+
from .wireless import devicectl, exposure
|
|
791
|
+
udid = self.pinned or self.settings().get("udid")
|
|
792
|
+
if hostnames is None:
|
|
793
|
+
try:
|
|
794
|
+
peer = devicectl.list_devices(timeout=4.0).get(str(udid).upper())
|
|
795
|
+
except (devicectl.DevicectlError, RuntimeError):
|
|
796
|
+
peer = None
|
|
797
|
+
hostnames = peer.hostnames if peer is not None else ()
|
|
798
|
+
_addresses, hits = exposure.exposed(hostnames)
|
|
799
|
+
with self.runner_lock:
|
|
800
|
+
if self.runner is not runner:
|
|
801
|
+
return # restarted while looking: the new start is checked on its own
|
|
802
|
+
self.exposure = hits or None
|
|
803
|
+
if hits: # stays stopped until the person starts it again, which checks it again
|
|
804
|
+
self._stop_runner(remember=True)
|
|
805
|
+
|
|
806
|
+
def _stop_runner(self, remember):
|
|
807
|
+
services = [service for service in (self.runner, self.relay) if service is not None]
|
|
808
|
+
for service in services:
|
|
809
|
+
signal_stop = getattr(service, "signal_stop", None)
|
|
810
|
+
if callable(signal_stop):
|
|
811
|
+
signal_stop() # both at once: the second never waits out the first's grace
|
|
812
|
+
for service in services:
|
|
813
|
+
service.stop()
|
|
814
|
+
self.runner = self.relay = None
|
|
815
|
+
self.transport = None
|
|
816
|
+
if remember:
|
|
817
|
+
self.save_settings(autostart=False)
|
|
818
|
+
|
|
819
|
+
def autostart(self):
|
|
820
|
+
"""Resume the runner if it was running before and can run now."""
|
|
821
|
+
try:
|
|
822
|
+
with self.runner_lock:
|
|
823
|
+
if (self.settings().get("autostart") and (self.runner is None or not self.runner.running)
|
|
824
|
+
and self.built() and self.device() and not self.wda_alive()):
|
|
825
|
+
first = self.runner is None and self.watchdog.last_restart_at is None
|
|
826
|
+
if not first and self.watchdog.admit(time.monotonic()) is not None:
|
|
827
|
+
return # a relaunch goes through the same admission and backoff as the supervisor's
|
|
828
|
+
try:
|
|
829
|
+
self._start_runner()
|
|
830
|
+
finally:
|
|
831
|
+
if not first:
|
|
832
|
+
self.watchdog.done(True)
|
|
833
|
+
except Exception:
|
|
834
|
+
pass
|
|
835
|
+
|
|
836
|
+
def rearm_on_attach(self):
|
|
837
|
+
"""The chosen phone was off the cable and is plugged in now: start the helper again at once (DR-77).
|
|
838
|
+
|
|
839
|
+
A helper started while the phone was away is an ``xcodebuild`` that waits for its destination, and it looks
|
|
840
|
+
for the phone only every half hour or so, so it never sees the cable go in. This sees the edge (the chosen
|
|
841
|
+
phone's UDID, not any phone, listed after it was not) and starts the runner through the same admission and
|
|
842
|
+
backoff as ``autostart``. It starts nothing when a helper already answers, when the phone was not seen away
|
|
843
|
+
first (a launch is ``autostart``'s), or when what was plugged in is another phone. The helper is the chosen
|
|
844
|
+
phone's by UDID, and every task is checked against the phone it names again before it acts
|
|
845
|
+
(wda.identity.verify)."""
|
|
846
|
+
settings = self.settings()
|
|
847
|
+
udid = self.pinned or settings.get("udid")
|
|
848
|
+
if not udid or not settings.get("autostart"):
|
|
849
|
+
self._attached_before = None
|
|
850
|
+
return False
|
|
851
|
+
now_attached = self.attached(udid)
|
|
852
|
+
before, self._attached_before = self._attached_before, now_attached
|
|
853
|
+
if before is not False or now_attached is not True:
|
|
854
|
+
return False
|
|
855
|
+
try:
|
|
856
|
+
with self.runner_lock:
|
|
857
|
+
if not (self.built() and not self.expired() and self.device() and not self.wda_ready(timeout=1.5)):
|
|
858
|
+
return False
|
|
859
|
+
if self.watchdog.admit(time.monotonic()) is not None:
|
|
860
|
+
return False
|
|
861
|
+
try:
|
|
862
|
+
self._start_runner()
|
|
863
|
+
finally:
|
|
864
|
+
self.watchdog.done(True)
|
|
865
|
+
return True
|
|
866
|
+
except Exception: # noqa: BLE001 -- the next attach (or autostart) tries again
|
|
867
|
+
return False
|
|
868
|
+
|
|
869
|
+
def probe(self, now=None):
|
|
870
|
+
"""Check that WDA can reach the phone's UI; restart a runner wedged for too long."""
|
|
871
|
+
now = time.monotonic() if now is None else now
|
|
872
|
+
if now - self.health["probed_at"] < PROBE_INTERVAL:
|
|
873
|
+
return
|
|
874
|
+
self.health["probed_at"] = now
|
|
875
|
+
ready = self.wda_ready()
|
|
876
|
+
self.health["ready"] = ready
|
|
877
|
+
if not ready:
|
|
878
|
+
self.health.update(responsive=None, locked=None, since=None)
|
|
879
|
+
# A runner Mobster started, whose process is up and past its start-up grace, that doesn't answer /status
|
|
880
|
+
# is wedged. A runner stopped on purpose, still starting or not ours is not. Its start-up grace counts from
|
|
881
|
+
# its newest launch: one held back while the phone was locked launches long after start_runner.
|
|
882
|
+
started = self.runner_started_at
|
|
883
|
+
launched = getattr(self.runner, "launched_at", None)
|
|
884
|
+
if started is not None and isinstance(launched, (int, float)):
|
|
885
|
+
started = max(started, launched)
|
|
886
|
+
if (self.runner is not None and self._runner_alive() and started is not None
|
|
887
|
+
and time.monotonic() - started >= STARTUP_GRACE_SECONDS):
|
|
888
|
+
self.watchdog.probe_result(False, now)
|
|
889
|
+
else:
|
|
890
|
+
self.watchdog.reset_failures()
|
|
891
|
+
return
|
|
892
|
+
try:
|
|
893
|
+
locked = self._wda_get("/wda/locked", PROBE_TIMEOUT).get("value")
|
|
894
|
+
self.health.update(responsive=True, locked=locked is True, since=None)
|
|
895
|
+
self.watchdog.probe_result(True, now)
|
|
896
|
+
return
|
|
897
|
+
except Exception:
|
|
898
|
+
since = self.health["since"] or now
|
|
899
|
+
self.health.update(responsive=False, locked=None, since=since)
|
|
900
|
+
if self.watchdog.probe_result(False, now):
|
|
901
|
+
self.health.update(since=None)
|
|
902
|
+
|
|
903
|
+
def _restart_wedged_runner(self):
|
|
904
|
+
self.start_runner() # stops the wedged runner and relay first
|
|
905
|
+
|
|
906
|
+
def poll_presence(self):
|
|
907
|
+
"""One lazy, read-only poll of whether the owner is using the phone, and the idle release."""
|
|
908
|
+
if not self.wda_ready():
|
|
909
|
+
self.presence.observe(ok=False)
|
|
910
|
+
return
|
|
911
|
+
self.presence_poller.poll()
|
|
912
|
+
if self.presence.should_release():
|
|
913
|
+
for hook in list(self.idle_hooks):
|
|
914
|
+
try:
|
|
915
|
+
hook()
|
|
916
|
+
except Exception: # noqa: BLE001
|
|
917
|
+
pass
|
|
918
|
+
|
|
919
|
+
def watch(self, stop):
|
|
920
|
+
next_round = 0.0
|
|
921
|
+
while not stop.is_set():
|
|
922
|
+
try:
|
|
923
|
+
self.probe()
|
|
924
|
+
except Exception:
|
|
925
|
+
pass
|
|
926
|
+
if time.monotonic() >= next_round:
|
|
927
|
+
next_round = time.monotonic() + WATCH_INTERVAL
|
|
928
|
+
self.watch_round()
|
|
929
|
+
stop.wait(PROBE_TICK)
|
|
930
|
+
|
|
931
|
+
def watch_round(self):
|
|
932
|
+
"""One pass of the watch loop's slower jobs. Each one's failure is its own."""
|
|
933
|
+
for job in (self.autostart, self.rearm_on_attach, self.poll_presence, self.check_exposure,
|
|
934
|
+
self.retry_locked_build):
|
|
935
|
+
try:
|
|
936
|
+
job()
|
|
937
|
+
except Exception:
|
|
938
|
+
pass
|
|
939
|
+
if self.renewal is not None:
|
|
940
|
+
try:
|
|
941
|
+
self.renewal.tick()
|
|
942
|
+
except Exception:
|
|
943
|
+
pass
|
|
944
|
+
|
|
945
|
+
def engine_reading(self):
|
|
946
|
+
from .wireless import transport
|
|
947
|
+
settings = self.settings()
|
|
948
|
+
udid = self.pinned or settings.get("udid")
|
|
949
|
+
state = self.runner_state(ready=self.health["ready"])
|
|
950
|
+
wifi = self.transport == "wifi"
|
|
951
|
+
return {"chosen": bool(udid), "built": self.built(), "expired": self.expired(),
|
|
952
|
+
"exposed": bool(self.exposure), "runner": state["state"], "holding": self.runner_waiting(),
|
|
953
|
+
"hint": self.runner_cause if state["state"] != "running" else None,
|
|
954
|
+
"locked": self.health["locked"], "responsive": self.health["responsive"],
|
|
955
|
+
"usb": None if wifi else self.attached(), "wifi": wifi,
|
|
956
|
+
# The link layer checks every 10 s (wireless.monitor.TICK); a Wi-Fi helper the probe finds silent is
|
|
957
|
+
# named at the probe's pace instead, so the cause is said inside engine_health.NAMED_WITHIN_S.
|
|
958
|
+
"wifi_lost": bool(wifi and (self.health["ready"] is False or udid and transport.lost(udid)))}
|
|
959
|
+
|
|
960
|
+
def begin_close(self):
|
|
961
|
+
self.watching.set()
|
|
962
|
+
for service in (self.runner, self.relay):
|
|
963
|
+
signal_stop = getattr(service, "signal_stop", None)
|
|
964
|
+
if callable(signal_stop):
|
|
965
|
+
signal_stop()
|
|
966
|
+
step = self.build_step
|
|
967
|
+
if step is not None:
|
|
968
|
+
try:
|
|
969
|
+
os.killpg(step.pid, signal.SIGTERM)
|
|
970
|
+
except OSError:
|
|
971
|
+
pass
|
|
972
|
+
|
|
973
|
+
def close(self):
|
|
974
|
+
self.watching.set()
|
|
975
|
+
step = self.build_step
|
|
976
|
+
if step is not None:
|
|
977
|
+
stop_group(step)
|
|
978
|
+
self.stop_runner()
|