mobster-cli 0.4.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mobile_agent/__init__.py +4 -0
- mobile_agent/__main__.py +598 -0
- mobile_agent/accounts/__init__.py +1 -0
- mobile_agent/accounts/context.py +80 -0
- mobile_agent/accounts/mcp.py +55 -0
- mobile_agent/accounts/pacing.py +318 -0
- mobile_agent/accounts/register.py +78 -0
- mobile_agent/accounts/routes.py +212 -0
- mobile_agent/accounts/routing.py +69 -0
- mobile_agent/accounts/screen.py +273 -0
- mobile_agent/accounts/stats.py +54 -0
- mobile_agent/accounts/store.py +394 -0
- mobile_agent/agent_hooks.py +123 -0
- mobile_agent/ai_plans/__init__.py +7 -0
- mobile_agent/ai_plans/agents.py +96 -0
- mobile_agent/ai_plans/claude_code.py +208 -0
- mobile_agent/ai_plans/cli.py +400 -0
- mobile_agent/ai_plans/codex.py +258 -0
- mobile_agent/ai_plans/compat_chat.py +307 -0
- mobile_agent/ai_plans/device_login.py +164 -0
- mobile_agent/ai_plans/draft_client.py +68 -0
- mobile_agent/ai_plans/gemini.py +137 -0
- mobile_agent/ai_plans/instructions.py +65 -0
- mobile_agent/ai_plans/mcp_http.py +160 -0
- mobile_agent/ai_plans/phone_tools.py +892 -0
- mobile_agent/ai_plans/providers.py +235 -0
- mobile_agent/ai_plans/routes.py +160 -0
- mobile_agent/ai_plans/runner.py +400 -0
- mobile_agent/ai_plans/vault.py +114 -0
- mobile_agent/alerts.py +337 -0
- mobile_agent/android/__init__.py +18 -0
- mobile_agent/android/adb.py +268 -0
- mobile_agent/android/conditions.py +128 -0
- mobile_agent/android/doctor.py +54 -0
- mobile_agent/android/driver.py +137 -0
- mobile_agent/android/emulator.py +216 -0
- mobile_agent/android/logs.py +85 -0
- mobile_agent/android/manager.py +225 -0
- mobile_agent/android/records.py +31 -0
- mobile_agent/android/tree.py +96 -0
- mobile_agent/answer_shapes.py +230 -0
- mobile_agent/api_devices.py +333 -0
- mobile_agent/api_errors.py +17 -0
- mobile_agent/api_get.py +249 -0
- mobile_agent/api_http.py +467 -0
- mobile_agent/api_post.py +453 -0
- mobile_agent/api_routes.py +173 -0
- mobile_agent/app_api/__init__.py +4 -0
- mobile_agent/app_api/cli.py +151 -0
- mobile_agent/app_api/endpoints.py +193 -0
- mobile_agent/app_api/mcp.py +54 -0
- mobile_agent/app_api/openapi.py +146 -0
- mobile_agent/app_api/reading.py +172 -0
- mobile_agent/app_api/register.py +10 -0
- mobile_agent/app_api/screen.py +216 -0
- mobile_agent/app_api/service.py +382 -0
- mobile_agent/app_api/sessions.py +404 -0
- mobile_agent/app_api/tokens.py +167 -0
- mobile_agent/app_choice.py +131 -0
- mobile_agent/app_icons.py +183 -0
- mobile_agent/app_killer.py +69 -0
- mobile_agent/artifacts.py +606 -0
- mobile_agent/attach/__init__.py +13 -0
- mobile_agent/attach/cli.py +46 -0
- mobile_agent/attach/flutter.py +28 -0
- mobile_agent/attach/score.py +119 -0
- mobile_agent/attach/web.py +13 -0
- mobile_agent/attachments/__init__.py +1 -0
- mobile_agent/attachments/agent.py +418 -0
- mobile_agent/attachments/extract.py +217 -0
- mobile_agent/attachments/mcp.py +104 -0
- mobile_agent/attachments/register.py +73 -0
- mobile_agent/attachments/routes.py +326 -0
- mobile_agent/attachments/service.py +170 -0
- mobile_agent/attachments/sniff.py +152 -0
- mobile_agent/attachments/store.py +441 -0
- mobile_agent/audit_verify.py +436 -0
- mobile_agent/bench/README.md +410 -0
- mobile_agent/bench/__init__.py +0 -0
- mobile_agent/bench/__main__.py +394 -0
- mobile_agent/bench/ab.py +168 -0
- mobile_agent/bench/actuation.py +368 -0
- mobile_agent/bench/agents/__init__.py +9 -0
- mobile_agent/bench/agents/base.py +105 -0
- mobile_agent/bench/agents/plan_clients.py +245 -0
- mobile_agent/bench/agents/smart.py +533 -0
- mobile_agent/bench/checks.py +358 -0
- mobile_agent/bench/decide_gate.py +214 -0
- mobile_agent/bench/diagnose.py +181 -0
- mobile_agent/bench/evals/__init__.py +14 -0
- mobile_agent/bench/evals/build.py +565 -0
- mobile_agent/bench/evals/checkpoint.py +214 -0
- mobile_agent/bench/evals/irt.py +219 -0
- mobile_agent/bench/evals/l0.py +647 -0
- mobile_agent/bench/evals/l1.py +866 -0
- mobile_agent/bench/evals/models.py +124 -0
- mobile_agent/bench/evals/pairwise.py +189 -0
- mobile_agent/bench/evals/proxy.py +90 -0
- mobile_agent/bench/evals/sets.py +149 -0
- mobile_agent/bench/evals/traces.py +446 -0
- mobile_agent/bench/fixtures.py +176 -0
- mobile_agent/bench/guard_replay.py +129 -0
- mobile_agent/bench/h2-manifest.json +20 -0
- mobile_agent/bench/h3-manifest.json +20 -0
- mobile_agent/bench/heldout-manifest.json +32 -0
- mobile_agent/bench/heldout.py +375 -0
- mobile_agent/bench/iosworld.py +901 -0
- mobile_agent/bench/iosworld_stats.py +510 -0
- mobile_agent/bench/llm_units.py +116 -0
- mobile_agent/bench/manifest.json +93 -0
- mobile_agent/bench/metrics.py +66 -0
- mobile_agent/bench/phone.py +44 -0
- mobile_agent/bench/replay.py +172 -0
- mobile_agent/bench/report.py +207 -0
- mobile_agent/bench/runner.py +456 -0
- mobile_agent/bench/safety.py +310 -0
- mobile_agent/bench/stream_timing.py +213 -0
- mobile_agent/bench/suite.py +444 -0
- mobile_agent/bench/sweep.py +176 -0
- mobile_agent/bench/tape.py +798 -0
- mobile_agent/bench/tape_replay.py +88 -0
- mobile_agent/bench/trace_evals.py +128 -0
- mobile_agent/bench/trace_view.py +81 -0
- mobile_agent/bench/truth/iphone15pro.json +138 -0
- mobile_agent/bench/truth.py +340 -0
- mobile_agent/bench/tuning.py +543 -0
- mobile_agent/bench/tuning_analysis.py +656 -0
- mobile_agent/bench/vague-heldout-manifest.json +12 -0
- mobile_agent/bench/vague_rubric.py +135 -0
- mobile_agent/bench/vague_run.py +243 -0
- mobile_agent/brand.py +454 -0
- mobile_agent/brand_fonts.py +756 -0
- mobile_agent/build_identity.py +21 -0
- mobile_agent/cable_bt.py +623 -0
- mobile_agent/cable_bt_runtime.py +128 -0
- mobile_agent/catalog.py +69 -0
- mobile_agent/chart_svg.py +415 -0
- mobile_agent/chat_here.py +189 -0
- mobile_agent/cli_constants.py +5 -0
- mobile_agent/cli_parser.py +611 -0
- mobile_agent/cli_run.py +115 -0
- mobile_agent/compiled_read/__init__.py +21 -0
- mobile_agent/compiled_read/delta.py +89 -0
- mobile_agent/compiled_read/extractors.py +202 -0
- mobile_agent/compiled_read/routes.py +56 -0
- mobile_agent/completion.py +267 -0
- mobile_agent/compose.py +48 -0
- mobile_agent/config.py +114 -0
- mobile_agent/connectors/__init__.py +1 -0
- mobile_agent/connectors/api_session.py +76 -0
- mobile_agent/connectors/callback_page.py +146 -0
- mobile_agent/connectors/client.py +582 -0
- mobile_agent/connectors/github_app.py +55 -0
- mobile_agent/connectors/graph_api.py +396 -0
- mobile_agent/connectors/imports.py +170 -0
- mobile_agent/connectors/issue.py +66 -0
- mobile_agent/connectors/keystore.py +85 -0
- mobile_agent/connectors/marks.py +222 -0
- mobile_agent/connectors/oauth.py +476 -0
- mobile_agent/connectors/picker.py +228 -0
- mobile_agent/connectors/presets.py +591 -0
- mobile_agent/connectors/register.py +48 -0
- mobile_agent/connectors/routes.py +153 -0
- mobile_agent/connectors/service.py +966 -0
- mobile_agent/connectors/shared.py +136 -0
- mobile_agent/connectors/skill.py +446 -0
- mobile_agent/connectors/slack_api.py +269 -0
- mobile_agent/connectors/steps.py +92 -0
- mobile_agent/connectors/store.py +144 -0
- mobile_agent/connectors/vault.py +72 -0
- mobile_agent/console.py +178 -0
- mobile_agent/content_queue/__init__.py +1 -0
- mobile_agent/content_queue/client.py +263 -0
- mobile_agent/content_queue/importing.py +153 -0
- mobile_agent/content_queue/media_check.py +224 -0
- mobile_agent/content_queue/prompt.py +64 -0
- mobile_agent/content_queue/register.py +91 -0
- mobile_agent/content_queue/routes.py +370 -0
- mobile_agent/content_queue/runner.py +475 -0
- mobile_agent/content_queue/store.py +521 -0
- mobile_agent/contract.py +996 -0
- mobile_agent/contract_asks.py +248 -0
- mobile_agent/contract_prompt.py +45 -0
- mobile_agent/contract_screen.py +981 -0
- mobile_agent/contract_view.py +84 -0
- mobile_agent/corpus/__init__.py +26 -0
- mobile_agent/corpus/cli.py +63 -0
- mobile_agent/corpus/consent.py +55 -0
- mobile_agent/corpus/listener.py +76 -0
- mobile_agent/corpus/register.py +9 -0
- mobile_agent/corpus/skeleton.py +72 -0
- mobile_agent/corpus/store.py +143 -0
- mobile_agent/costs.py +161 -0
- mobile_agent/decider_scope.py +40 -0
- mobile_agent/decision_provider.py +189 -0
- mobile_agent/deeplinks.py +352 -0
- mobile_agent/device_info.py +116 -0
- mobile_agent/device_manager.py +978 -0
- mobile_agent/device_status.py +195 -0
- mobile_agent/device_targets.py +217 -0
- mobile_agent/devices.py +676 -0
- mobile_agent/devkit/__init__.py +64 -0
- mobile_agent/devkit/account_cli.py +90 -0
- mobile_agent/devkit/asc.py +562 -0
- mobile_agent/devkit/asc_cli.py +214 -0
- mobile_agent/devkit/ci.py +737 -0
- mobile_agent/devkit/ci_cli.py +196 -0
- mobile_agent/devkit/ci_hosts.py +194 -0
- mobile_agent/devkit/es256.py +197 -0
- mobile_agent/devkit/expo.py +440 -0
- mobile_agent/devkit/expo_cli.py +405 -0
- mobile_agent/devkit/expo_dev.py +203 -0
- mobile_agent/devkit/expo_routes.py +90 -0
- mobile_agent/devkit/fixtures_cli.py +133 -0
- mobile_agent/devkit/fixtures_routes.py +82 -0
- mobile_agent/devkit/links.py +375 -0
- mobile_agent/devkit/links_cli.py +123 -0
- mobile_agent/devkit/links_routes.py +83 -0
- mobile_agent/devkit/nightly.py +152 -0
- mobile_agent/devkit/shots.py +203 -0
- mobile_agent/devkit/testflight.py +155 -0
- mobile_agent/devkit/testflight_cli.py +127 -0
- mobile_agent/devkit/testflight_routes.py +111 -0
- mobile_agent/devkit/xcode.py +236 -0
- mobile_agent/devkit/xcode_cli.py +101 -0
- mobile_agent/devtools.py +419 -0
- mobile_agent/doctor.py +509 -0
- mobile_agent/doctor_fix.py +467 -0
- mobile_agent/downloads.py +55 -0
- mobile_agent/drivers.py +793 -0
- mobile_agent/dry_run.py +102 -0
- mobile_agent/effect_ledger.py +83 -0
- mobile_agent/engine_health.py +378 -0
- mobile_agent/engine_hooks.py +96 -0
- mobile_agent/engines.py +408 -0
- mobile_agent/env_file.py +63 -0
- mobile_agent/env_secrets.py +149 -0
- mobile_agent/errors.py +15 -0
- mobile_agent/evals/__init__.py +3 -0
- mobile_agent/evals/app_survey.py +149 -0
- mobile_agent/evals/development/__init__.py +1 -0
- mobile_agent/evals/development/score.py +276 -0
- mobile_agent/evals/latency_report.py +312 -0
- mobile_agent/evals/offline_report.py +374 -0
- mobile_agent/evals/oracles.py +298 -0
- mobile_agent/evals/redteam/__init__.py +15 -0
- mobile_agent/evals/redteam/__main__.py +72 -0
- mobile_agent/evals/redteam/cases.py +730 -0
- mobile_agent/evals/redteam/harness.py +604 -0
- mobile_agent/evals/redteam/live.py +139 -0
- mobile_agent/evals/redteam/plain.py +85 -0
- mobile_agent/evals/vision/__init__.py +0 -0
- mobile_agent/evals/vision/images/dog_001.jpg +0 -0
- mobile_agent/evals/vision/images/dog_002.jpg +0 -0
- mobile_agent/evals/vision/images/dog_003.jpg +0 -0
- mobile_agent/evals/vision/images/dog_004.jpg +0 -0
- mobile_agent/evals/vision/images/dog_005.jpg +0 -0
- mobile_agent/evals/vision/images/dog_006.jpg +0 -0
- mobile_agent/evals/vision/images/dog_007.jpg +0 -0
- mobile_agent/evals/vision/images/dog_010.jpg +0 -0
- mobile_agent/evals/vision/images/dog_011.jpg +0 -0
- mobile_agent/evals/vision/images/dog_013.jpg +0 -0
- mobile_agent/evals/vision/images/dog_014.jpg +0 -0
- mobile_agent/evals/vision/images/dog_015.jpg +0 -0
- mobile_agent/evals/vision/images/dog_016.jpg +0 -0
- mobile_agent/evals/vision/images/dog_017.jpg +0 -0
- mobile_agent/evals/vision/images/dog_019.jpg +0 -0
- mobile_agent/evals/vision/images/dog_020.jpg +0 -0
- mobile_agent/evals/vision/images/dog_021.jpg +0 -0
- mobile_agent/evals/vision/images/dog_022.jpg +0 -0
- mobile_agent/evals/vision/images/dog_023.jpg +0 -0
- mobile_agent/evals/vision/images/dog_024.jpg +0 -0
- mobile_agent/evals/vision/images/dog_026.jpg +0 -0
- mobile_agent/evals/vision/images/dog_027.jpg +0 -0
- mobile_agent/evals/vision/images/dog_034.jpg +0 -0
- mobile_agent/evals/vision/images/dog_035.jpg +0 -0
- mobile_agent/evals/vision/images/dog_038.jpg +0 -0
- mobile_agent/evals/vision/images/dog_042.jpg +0 -0
- mobile_agent/evals/vision/images/dog_047.jpg +0 -0
- mobile_agent/evals/vision/images/dog_052.jpg +0 -0
- mobile_agent/evals/vision/images/dog_058.jpg +0 -0
- mobile_agent/evals/vision/images/dog_062.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_000.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_010.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_011.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_015.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_016.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_017.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_019.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_021.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_025.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_026.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_027.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_030.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_032.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_033.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_034.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_035.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_036.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_037.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_039.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_040.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_041.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_042.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_043.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_044.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_046.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_047.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_048.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_049.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_051.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_053.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_054.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_055.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_058.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_002.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_003.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_004.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_007.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_009.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_010.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_011.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_012.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_013.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_014.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_016.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_017.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_023.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_027.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_029.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_032.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_033.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_036.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_041.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_043.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_047.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_049.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_051.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_053.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_056.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_057.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_058.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_060.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_061.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_062.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_063.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_064.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_065.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_066.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_067.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_068.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_069.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_070.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_071.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_000.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_001.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_002.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_004.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_005.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_008.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_009.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_010.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_012.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_013.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_015.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_016.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_017.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_018.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_021.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_022.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_023.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_024.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_030.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_031.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_032.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_033.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_034.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_036.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_038.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_039.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_040.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_041.jpg +0 -0
- mobile_agent/evals/vision/manifest.json +2355 -0
- mobile_agent/eventbus.py +110 -0
- mobile_agent/explain.py +34 -0
- mobile_agent/failure_line.py +197 -0
- mobile_agent/fanout.py +663 -0
- mobile_agent/feedback/__init__.py +2 -0
- mobile_agent/feedback/bundle.py +204 -0
- mobile_agent/feedback/outbox.py +196 -0
- mobile_agent/feedback/register.py +9 -0
- mobile_agent/feedback/routes.py +177 -0
- mobile_agent/fleet.py +841 -0
- mobile_agent/fleet_health.py +729 -0
- mobile_agent/fleet_phone.py +186 -0
- mobile_agent/frontier/__init__.py +79 -0
- mobile_agent/frontier/act.py +380 -0
- mobile_agent/frontier/actions.py +70 -0
- mobile_agent/frontier/agent.py +256 -0
- mobile_agent/frontier/approvals.py +366 -0
- mobile_agent/frontier/hooks.py +199 -0
- mobile_agent/frontier/interrupts.py +125 -0
- mobile_agent/frontier/observe.py +331 -0
- mobile_agent/frontier/phases.py +381 -0
- mobile_agent/frontier/pipeline.py +61 -0
- mobile_agent/frontier/prompt.py +384 -0
- mobile_agent/frontier/protocol.py +91 -0
- mobile_agent/frontier/safety.py +467 -0
- mobile_agent/frontier/saved.py +375 -0
- mobile_agent/frontier/screen.py +419 -0
- mobile_agent/frontier/skills.py +147 -0
- mobile_agent/frontier/stages/__init__.py +1 -0
- mobile_agent/frontier/stages/after.py +192 -0
- mobile_agent/frontier/stages/approve.py +138 -0
- mobile_agent/frontier/stages/controls.py +115 -0
- mobile_agent/frontier/stages/ends.py +102 -0
- mobile_agent/frontier/stages/gate.py +92 -0
- mobile_agent/frontier/stages/guards.py +132 -0
- mobile_agent/frontier/stages/points.py +155 -0
- mobile_agent/frontier/stages/send.py +177 -0
- mobile_agent/frontier/turn.py +151 -0
- mobile_agent/full_disk_access.py +83 -0
- mobile_agent/gemini.py +69 -0
- mobile_agent/genuine.py +120 -0
- mobile_agent/graph/__init__.py +4 -0
- mobile_agent/graph/catalog.py +789 -0
- mobile_agent/graph/chain.py +306 -0
- mobile_agent/graph/dispatch.py +98 -0
- mobile_agent/graph/draft.py +373 -0
- mobile_agent/graph/edit.py +76 -0
- mobile_agent/graph/estimate.py +189 -0
- mobile_agent/graph/expr.py +562 -0
- mobile_agent/graph/functions.py +271 -0
- mobile_agent/graph/infer.py +144 -0
- mobile_agent/graph/nodes.py +936 -0
- mobile_agent/graph/phone_steps.py +178 -0
- mobile_agent/graph/rerun.py +327 -0
- mobile_agent/graph/run.py +899 -0
- mobile_agent/graph/schema.py +842 -0
- mobile_agent/graph/state.py +161 -0
- mobile_agent/graph/values.py +198 -0
- mobile_agent/harness/__init__.py +2 -0
- mobile_agent/harness/checkpoints.py +468 -0
- mobile_agent/harness/control.py +101 -0
- mobile_agent/harness/prewarm.py +213 -0
- mobile_agent/harness/register.py +43 -0
- mobile_agent/harness/routes.py +114 -0
- mobile_agent/harness/settings.py +66 -0
- mobile_agent/harness/tools/__init__.py +67 -0
- mobile_agent/harness/tools/ask_user.py +102 -0
- mobile_agent/harness/tools/watch.py +120 -0
- mobile_agent/harness_api.py +815 -0
- mobile_agent/hedge.py +203 -0
- mobile_agent/help_topics.py +305 -0
- mobile_agent/hints.py +248 -0
- mobile_agent/history.py +69 -0
- mobile_agent/home.py +214 -0
- mobile_agent/http_pool.py +175 -0
- mobile_agent/image_policy.py +139 -0
- mobile_agent/inbox.py +148 -0
- mobile_agent/installed_apps.py +344 -0
- mobile_agent/integrations/__init__.py +1 -0
- mobile_agent/integrations/cli.py +472 -0
- mobile_agent/integrations/client_files.py +65 -0
- mobile_agent/integrations/clients.py +998 -0
- mobile_agent/integrations/doctor.py +185 -0
- mobile_agent/integrations/jsonc.py +383 -0
- mobile_agent/integrations/routes.py +135 -0
- mobile_agent/integrations/rules.py +112 -0
- mobile_agent/integrations/server_options.py +46 -0
- mobile_agent/integrations/skill.py +152 -0
- mobile_agent/integrations/skill_files.py +10 -0
- mobile_agent/integrations/textedit.py +309 -0
- mobile_agent/intents/__init__.py +17 -0
- mobile_agent/intents/atlas.py +216 -0
- mobile_agent/intents/callback.py +44 -0
- mobile_agent/intents/cli.py +55 -0
- mobile_agent/intents/harvest.py +91 -0
- mobile_agent/intents/plan.py +93 -0
- mobile_agent/ios/__init__.py +1 -0
- mobile_agent/ios/guide.py +16 -0
- mobile_agent/ios/kb.py +212 -0
- mobile_agent/ios/kb_data.py +1588 -0
- mobile_agent/ios/mcp.py +44 -0
- mobile_agent/ios/register.py +8 -0
- mobile_agent/ios/system.py +369 -0
- mobile_agent/journal.py +531 -0
- mobile_agent/keychain_acl.py +374 -0
- mobile_agent/keys.py +395 -0
- mobile_agent/latency_overlap.py +36 -0
- mobile_agent/latency_trace.py +170 -0
- mobile_agent/learned/__init__.py +15 -0
- mobile_agent/learned/app_path.py +160 -0
- mobile_agent/learned/code_read.py +38 -0
- mobile_agent/learned/parts.py +546 -0
- mobile_agent/learned/programs.py +691 -0
- mobile_agent/learned/register.py +171 -0
- mobile_agent/learned/routes.py +61 -0
- mobile_agent/learned/taught.py +329 -0
- mobile_agent/learned/teach.py +538 -0
- mobile_agent/learned/workflows.py +647 -0
- mobile_agent/learned/zero.py +41 -0
- mobile_agent/links.py +37 -0
- mobile_agent/listen.py +607 -0
- mobile_agent/lock_spike.py +278 -0
- mobile_agent/lockscreen.py +526 -0
- mobile_agent/login.py +354 -0
- mobile_agent/longform/__init__.py +15 -0
- mobile_agent/longform/ads.py +191 -0
- mobile_agent/longform/blocks.py +117 -0
- mobile_agent/longform/budget.py +265 -0
- mobile_agent/longform/context.py +285 -0
- mobile_agent/longform/costs.py +93 -0
- mobile_agent/longform/digest.py +145 -0
- mobile_agent/longform/extract.py +508 -0
- mobile_agent/longform/feed.py +505 -0
- mobile_agent/longform/followup.py +168 -0
- mobile_agent/longform/frames.py +82 -0
- mobile_agent/longform/identity.py +87 -0
- mobile_agent/longform/inbox.py +221 -0
- mobile_agent/longform/items.py +425 -0
- mobile_agent/longform/models.py +115 -0
- mobile_agent/longform/notebook.py +329 -0
- mobile_agent/longform/plan.py +614 -0
- mobile_agent/longform/recall.py +195 -0
- mobile_agent/longform/receipts.py +81 -0
- mobile_agent/longform/register.py +124 -0
- mobile_agent/longform/report.py +343 -0
- mobile_agent/longform/routes.py +102 -0
- mobile_agent/longform/session.py +644 -0
- mobile_agent/longform/store.py +146 -0
- mobile_agent/longform/synthesis.py +347 -0
- mobile_agent/mac_tools.py +223 -0
- mobile_agent/manual_control.py +143 -0
- mobile_agent/maps.py +149 -0
- mobile_agent/marks.py +92 -0
- mobile_agent/mcp_server/__init__.py +3 -0
- mobile_agent/mcp_server/__main__.py +29 -0
- mobile_agent/mcp_server/action_journal.py +24 -0
- mobile_agent/mcp_server/action_tools.py +638 -0
- mobile_agent/mcp_server/approvals.py +219 -0
- mobile_agent/mcp_server/catalog.py +92 -0
- mobile_agent/mcp_server/cli.py +194 -0
- mobile_agent/mcp_server/codes.py +147 -0
- mobile_agent/mcp_server/device_tools.py +306 -0
- mobile_agent/mcp_server/direct.py +272 -0
- mobile_agent/mcp_server/expo_tools.py +105 -0
- mobile_agent/mcp_server/phone_tools.py +510 -0
- mobile_agent/mcp_server/protocol.py +364 -0
- mobile_agent/mcp_server/registry.py +55 -0
- mobile_agent/mcp_server/run_tools.py +531 -0
- mobile_agent/mcp_server/schemas.py +425 -0
- mobile_agent/mcp_server/screen_tools.py +168 -0
- mobile_agent/mcp_server/session.py +469 -0
- mobile_agent/mcp_server/support.py +145 -0
- mobile_agent/mcp_server/tools.py +493 -0
- mobile_agent/memory/__init__.py +9 -0
- mobile_agent/memory/cli.py +207 -0
- mobile_agent/memory/extract.py +191 -0
- mobile_agent/memory/listener.py +100 -0
- mobile_agent/memory/model.py +144 -0
- mobile_agent/memory/project.py +96 -0
- mobile_agent/memory/provider.py +127 -0
- mobile_agent/memory/register.py +21 -0
- mobile_agent/memory/retrieval.py +172 -0
- mobile_agent/memory/routes.py +216 -0
- mobile_agent/memory/runs.py +124 -0
- mobile_agent/memory/store.py +806 -0
- mobile_agent/memory/writer.py +278 -0
- mobile_agent/mirroring.py +585 -0
- mobile_agent/mirroring_runtime.py +221 -0
- mobile_agent/model_clients.py +604 -0
- mobile_agent/narrate.py +813 -0
- mobile_agent/native_helpers.py +84 -0
- mobile_agent/netxray/__init__.py +1 -0
- mobile_agent/netxray/analyze.py +322 -0
- mobile_agent/netxray/attribution.py +62 -0
- mobile_agent/netxray/capture.py +88 -0
- mobile_agent/netxray/cli.py +142 -0
- mobile_agent/netxray/job.py +219 -0
- mobile_agent/netxray/mobster-pcap.c +560 -0
- mobile_agent/netxray/packets.py +410 -0
- mobile_agent/netxray/pcapng.py +223 -0
- mobile_agent/netxray/register.py +93 -0
- mobile_agent/netxray/report.py +214 -0
- mobile_agent/netxray/store.py +48 -0
- mobile_agent/netxray/summary.py +157 -0
- mobile_agent/netxray/target.py +67 -0
- mobile_agent/netxray/tool.py +66 -0
- mobile_agent/netxray/trackers.py +194 -0
- mobile_agent/notify_triggers/__init__.py +1 -0
- mobile_agent/notify_triggers/rules.py +39 -0
- mobile_agent/notify_triggers/sources.py +315 -0
- mobile_agent/notify_triggers/watch.py +135 -0
- mobile_agent/offers.py +86 -0
- mobile_agent/orchestrate.py +215 -0
- mobile_agent/outlets/__init__.py +1 -0
- mobile_agent/outlets/connect.py +249 -0
- mobile_agent/outlets/folder.py +180 -0
- mobile_agent/outlets/hooks.py +453 -0
- mobile_agent/outlets/inbound.py +472 -0
- mobile_agent/outlets/inbound_reply.py +57 -0
- mobile_agent/outlets/inbound_routes.py +81 -0
- mobile_agent/outlets/kinds.py +308 -0
- mobile_agent/outlets/mail.py +110 -0
- mobile_agent/outlets/net.py +86 -0
- mobile_agent/outlets/notion.py +244 -0
- mobile_agent/outlets/register.py +56 -0
- mobile_agent/outlets/routes.py +135 -0
- mobile_agent/outlets/service.py +580 -0
- mobile_agent/outlets/sheets.py +169 -0
- mobile_agent/outlets/slack.py +177 -0
- mobile_agent/outlets/store.py +194 -0
- mobile_agent/outlets/vault.py +80 -0
- mobile_agent/outlets/webhook.py +316 -0
- mobile_agent/output_contract.py +141 -0
- mobile_agent/owner_presence.py +211 -0
- mobile_agent/ownership.py +219 -0
- mobile_agent/packs/__init__.py +1 -0
- mobile_agent/packs/calendar.py +243 -0
- mobile_agent/packs/context.py +68 -0
- mobile_agent/packs/core.py +757 -0
- mobile_agent/packs/front_door.py +316 -0
- mobile_agent/packs/heal.py +214 -0
- mobile_agent/packs/health.py +122 -0
- mobile_agent/packs/insights.py +76 -0
- mobile_agent/packs/instagram.py +640 -0
- mobile_agent/packs/linkedin.py +106 -0
- mobile_agent/packs/maps.py +196 -0
- mobile_agent/packs/mcp.py +149 -0
- mobile_agent/packs/messages.py +270 -0
- mobile_agent/packs/notes.py +226 -0
- mobile_agent/packs/photos.py +166 -0
- mobile_agent/packs/pins.py +47 -0
- mobile_agent/packs/reddit.py +110 -0
- mobile_agent/packs/register.py +276 -0
- mobile_agent/packs/rows.py +108 -0
- mobile_agent/packs/safari.py +143 -0
- mobile_agent/packs/settings.py +253 -0
- mobile_agent/packs/social.py +300 -0
- mobile_agent/packs/threads.py +97 -0
- mobile_agent/packs/tiktok.py +455 -0
- mobile_agent/packs/tools.py +290 -0
- mobile_agent/packs/x.py +127 -0
- mobile_agent/packs/youtube.py +199 -0
- mobile_agent/parity.py +270 -0
- mobile_agent/paths.py +99 -0
- mobile_agent/perception.py +470 -0
- mobile_agent/permissions/__init__.py +32 -0
- mobile_agent/permissions/consequence.py +535 -0
- mobile_agent/permissions/exceptions.py +99 -0
- mobile_agent/permissions/gate.py +316 -0
- mobile_agent/permissions/outside.py +316 -0
- mobile_agent/permissions/policy.py +288 -0
- mobile_agent/permissions/project_policy.py +411 -0
- mobile_agent/permissions/register.py +148 -0
- mobile_agent/permissions/rules.py +291 -0
- mobile_agent/phone_io/__init__.py +146 -0
- mobile_agent/phone_io/cli.py +427 -0
- mobile_agent/phone_io/clipboard.py +78 -0
- mobile_agent/phone_io/controls.py +78 -0
- mobile_agent/phone_io/files.py +289 -0
- mobile_agent/phone_io/handoff.py +123 -0
- mobile_agent/phone_io/icloud.py +118 -0
- mobile_agent/phone_io/install.py +188 -0
- mobile_agent/phone_io/network_capture.py +188 -0
- mobile_agent/phone_io/network_cli.py +138 -0
- mobile_agent/phone_io/network_profiles.py +138 -0
- mobile_agent/phone_io/photos.py +512 -0
- mobile_agent/phone_io/recording.py +115 -0
- mobile_agent/phone_io/secrets.py +146 -0
- mobile_agent/phone_io/transfer.py +895 -0
- mobile_agent/phone_io/tunnel.py +129 -0
- mobile_agent/phone_ready.py +91 -0
- mobile_agent/pinning.py +158 -0
- mobile_agent/plan_gate.py +807 -0
- mobile_agent/plugins/__init__.py +3 -0
- mobile_agent/plugins/install.py +163 -0
- mobile_agent/plugins/manifest.py +242 -0
- mobile_agent/plugins/register.py +15 -0
- mobile_agent/plugins/routes.py +82 -0
- mobile_agent/plugins/service.py +322 -0
- mobile_agent/plugins/skills.py +76 -0
- mobile_agent/plugins/store.py +79 -0
- mobile_agent/pool.py +285 -0
- mobile_agent/power_watch.py +275 -0
- mobile_agent/premium_model.py +22 -0
- mobile_agent/project_env.py +291 -0
- mobile_agent/project_env_cli.py +74 -0
- mobile_agent/project_env_routes.py +55 -0
- mobile_agent/projects/__init__.py +27 -0
- mobile_agent/projects/ci.py +332 -0
- mobile_agent/projects/clone.py +339 -0
- mobile_agent/projects/drafts.py +158 -0
- mobile_agent/projects/folder.py +436 -0
- mobile_agent/projects/github.py +131 -0
- mobile_agent/projects/register.py +31 -0
- mobile_agent/projects/revisions.py +253 -0
- mobile_agent/projects/routes.py +234 -0
- mobile_agent/projects/runs.py +297 -0
- mobile_agent/projects/service.py +695 -0
- mobile_agent/projects/setup.py +84 -0
- mobile_agent/projects/store.py +379 -0
- mobile_agent/projects/sync.py +380 -0
- mobile_agent/projects/workflow_files.py +270 -0
- mobile_agent/replay_core/__init__.py +26 -0
- mobile_agent/replay_core/checks.py +594 -0
- mobile_agent/replay_core/engine.py +596 -0
- mobile_agent/replay_core/heal.py +364 -0
- mobile_agent/replay_core/path.py +240 -0
- mobile_agent/replay_core/recorder.py +206 -0
- mobile_agent/replay_core/store.py +171 -0
- mobile_agent/result_blocks.py +941 -0
- mobile_agent/run.py +642 -0
- mobile_agent/run_delivery.py +123 -0
- mobile_agent/run_executor.py +200 -0
- mobile_agent/run_export.py +284 -0
- mobile_agent/run_gif.py +891 -0
- mobile_agent/runner_hints.py +86 -0
- mobile_agent/runner_renewal.py +205 -0
- mobile_agent/runtime_create.py +549 -0
- mobile_agent/runtime_driver.py +458 -0
- mobile_agent/runtime_limits.py +319 -0
- mobile_agent/runtime_queue.py +394 -0
- mobile_agent/runtime_runs.py +383 -0
- mobile_agent/runtime_status.py +181 -0
- mobile_agent/runtime_teach.py +295 -0
- mobile_agent/runtime_work.py +599 -0
- mobile_agent/screen_text.py +101 -0
- mobile_agent/secret_filter.py +113 -0
- mobile_agent/serve.py +160 -0
- mobile_agent/server.py +363 -0
- mobile_agent/service_support.py +42 -0
- mobile_agent/settle/__init__.py +1 -0
- mobile_agent/settle/predict.py +102 -0
- mobile_agent/settle/replay.py +92 -0
- mobile_agent/settle/store.py +93 -0
- mobile_agent/settle/stream.py +43 -0
- mobile_agent/setup_cli.py +225 -0
- mobile_agent/setup_errors.py +87 -0
- mobile_agent/setup_service.py +527 -0
- mobile_agent/share.py +104 -0
- mobile_agent/signing.py +184 -0
- mobile_agent/sim/__init__.py +6 -0
- mobile_agent/sim/__main__.py +26 -0
- mobile_agent/sim/api.py +952 -0
- mobile_agent/sim/apps.py +172 -0
- mobile_agent/sim/biometrics.py +84 -0
- mobile_agent/sim/cli.py +271 -0
- mobile_agent/sim/doctor.py +293 -0
- mobile_agent/sim/governor.py +90 -0
- mobile_agent/sim/holders.py +88 -0
- mobile_agent/sim/models.py +62 -0
- mobile_agent/sim/registry.py +161 -0
- mobile_agent/sim/simctl.py +269 -0
- mobile_agent/sim/wda.py +618 -0
- mobile_agent/sim/xcode.py +74 -0
- mobile_agent/skills/__init__.py +12 -0
- mobile_agent/skills/codes.py +597 -0
- mobile_agent/skills/notifications.py +281 -0
- mobile_agent/skills/open_url.py +127 -0
- mobile_agent/smart_events.py +588 -0
- mobile_agent/smart_result.py +490 -0
- mobile_agent/smart_run.py +413 -0
- mobile_agent/state.py +662 -0
- mobile_agent/states.py +217 -0
- mobile_agent/steering.py +70 -0
- mobile_agent/style.py +178 -0
- mobile_agent/supervised.py +234 -0
- mobile_agent/swift/helper_bt_hid.swift +534 -0
- mobile_agent/swift/helper_doc_text.swift +184 -0
- mobile_agent/swift/helper_draft_reply.swift +170 -0
- mobile_agent/swift/helper_map.swift +110 -0
- mobile_agent/swift/helper_mirroring.swift +1099 -0
- mobile_agent/swift/helper_mjpeg_mp4.swift +103 -0
- mobile_agent/swift/helper_pdf.swift +100 -0
- mobile_agent/swift/helper_phone_audio.swift +993 -0
- mobile_agent/swift/helper_phone_screen.swift +416 -0
- mobile_agent/swift/helper_screen_text.swift +24 -0
- mobile_agent/swift/helper_transfer.swift +246 -0
- mobile_agent/swift/ocr.swift +22 -0
- mobile_agent/task_policy.py +337 -0
- mobile_agent/team_signing/__init__.py +34 -0
- mobile_agent/team_signing/apple_ca.py +37 -0
- mobile_agent/team_signing/bundle.py +136 -0
- mobile_agent/team_signing/cli.py +208 -0
- mobile_agent/team_signing/identity.py +248 -0
- mobile_agent/team_signing/ledger.py +105 -0
- mobile_agent/team_signing/prebuilt.py +61 -0
- mobile_agent/team_signing/resign.py +256 -0
- mobile_agent/team_signing/service.py +382 -0
- mobile_agent/team_signing/tooling.py +243 -0
- mobile_agent/team_signing/writes.py +268 -0
- mobile_agent/terminal_image.py +81 -0
- mobile_agent/testkit/__init__.py +1 -0
- mobile_agent/testkit/a11y_baseline.py +132 -0
- mobile_agent/testkit/api.py +403 -0
- mobile_agent/testkit/capsule.py +400 -0
- mobile_agent/testkit/capsule_cli.py +57 -0
- mobile_agent/testkit/cli.py +948 -0
- mobile_agent/testkit/cli_arguments.py +193 -0
- mobile_agent/testkit/comparison.py +288 -0
- mobile_agent/testkit/comparison_cli.py +41 -0
- mobile_agent/testkit/discover.py +257 -0
- mobile_agent/testkit/editor.py +243 -0
- mobile_agent/testkit/execute.py +515 -0
- mobile_agent/testkit/github_checks.py +248 -0
- mobile_agent/testkit/gitref.py +66 -0
- mobile_agent/testkit/heal_review.py +176 -0
- mobile_agent/testkit/health.py +480 -0
- mobile_agent/testkit/history.py +255 -0
- mobile_agent/testkit/hosted_results.py +321 -0
- mobile_agent/testkit/input_files.py +173 -0
- mobile_agent/testkit/jobs.py +285 -0
- mobile_agent/testkit/junit_verdict.py +33 -0
- mobile_agent/testkit/maestro.py +802 -0
- mobile_agent/testkit/mcp.py +562 -0
- mobile_agent/testkit/mcp_comparison.py +88 -0
- mobile_agent/testkit/minimize.py +471 -0
- mobile_agent/testkit/minimizer_cli.py +48 -0
- mobile_agent/testkit/notify.py +212 -0
- mobile_agent/testkit/phone_video.py +191 -0
- mobile_agent/testkit/pr.py +491 -0
- mobile_agent/testkit/pr_report.py +562 -0
- mobile_agent/testkit/prbuild.py +148 -0
- mobile_agent/testkit/proof_report.py +55 -0
- mobile_agent/testkit/provenance.py +119 -0
- mobile_agent/testkit/prrun.py +244 -0
- mobile_agent/testkit/recent.py +166 -0
- mobile_agent/testkit/recipes.py +146 -0
- mobile_agent/testkit/record.py +204 -0
- mobile_agent/testkit/register.py +288 -0
- mobile_agent/testkit/reports.py +998 -0
- mobile_agent/testkit/repro.py +432 -0
- mobile_agent/testkit/scenario.py +440 -0
- mobile_agent/testkit/scenario_cli.py +37 -0
- mobile_agent/testkit/snapshots_cli.py +60 -0
- mobile_agent/testkit/suite.py +670 -0
- mobile_agent/testkit/targets.py +497 -0
- mobile_agent/testkit/xctest.py +378 -0
- mobile_agent/threads/__init__.py +1 -0
- mobile_agent/threads/chat_once.py +264 -0
- mobile_agent/threads/cli.py +740 -0
- mobile_agent/threads/client.py +326 -0
- mobile_agent/threads/context.py +246 -0
- mobile_agent/threads/mcp.py +739 -0
- mobile_agent/threads/recent.py +34 -0
- mobile_agent/threads/register.py +54 -0
- mobile_agent/threads/routes.py +188 -0
- mobile_agent/threads/service.py +975 -0
- mobile_agent/threads/store.py +361 -0
- mobile_agent/threads/summary.py +117 -0
- mobile_agent/threads/term.py +101 -0
- mobile_agent/threads/workflow_cli.py +104 -0
- mobile_agent/tls.py +36 -0
- mobile_agent/traces.py +271 -0
- mobile_agent/tracks.py +235 -0
- mobile_agent/transport.py +271 -0
- mobile_agent/triggers/__init__.py +1 -0
- mobile_agent/triggers/actions.py +204 -0
- mobile_agent/triggers/model.py +146 -0
- mobile_agent/triggers/register.py +29 -0
- mobile_agent/triggers/routes.py +101 -0
- mobile_agent/triggers/service.py +387 -0
- mobile_agent/triggers/store.py +142 -0
- mobile_agent/tui/__init__.py +89 -0
- mobile_agent/tui/app.py +582 -0
- mobile_agent/tui/chrome.py +321 -0
- mobile_agent/tui/commands.py +375 -0
- mobile_agent/tui/demo_phone.py +369 -0
- mobile_agent/tui/images.py +29 -0
- mobile_agent/tui/keys.py +64 -0
- mobile_agent/tui/prompt.py +195 -0
- mobile_agent/tui/render.py +395 -0
- mobile_agent/tui/screens.py +180 -0
- mobile_agent/tui/session.py +308 -0
- mobile_agent/tui/tasks.py +287 -0
- mobile_agent/tui/theme.py +118 -0
- mobile_agent/tui/widgets.py +272 -0
- mobile_agent/update_check.py +213 -0
- mobile_agent/usage.py +184 -0
- mobile_agent/verify/__init__.py +0 -0
- mobile_agent/verify/__main__.py +41 -0
- mobile_agent/verify/a11y.py +189 -0
- mobile_agent/verify/actions.py +290 -0
- mobile_agent/verify/assertions.py +570 -0
- mobile_agent/verify/audit.py +312 -0
- mobile_agent/verify/business.py +221 -0
- mobile_agent/verify/business_run.py +131 -0
- mobile_agent/verify/business_state.py +168 -0
- mobile_agent/verify/check_schema.py +398 -0
- mobile_agent/verify/checks.py +614 -0
- mobile_agent/verify/cli.py +423 -0
- mobile_agent/verify/conditions.py +483 -0
- mobile_agent/verify/crashes.py +321 -0
- mobile_agent/verify/device_audit.py +246 -0
- mobile_agent/verify/dialogs.py +287 -0
- mobile_agent/verify/evidence.py +705 -0
- mobile_agent/verify/fixtures.py +305 -0
- mobile_agent/verify/flow.py +213 -0
- mobile_agent/verify/l10n.py +141 -0
- mobile_agent/verify/netcerts.py +67 -0
- mobile_agent/verify/netproxy.py +356 -0
- mobile_agent/verify/netrun.py +81 -0
- mobile_agent/verify/netshim.m +28 -0
- mobile_agent/verify/netshim.py +48 -0
- mobile_agent/verify/netstubs.py +132 -0
- mobile_agent/verify/overlay.py +174 -0
- mobile_agent/verify/perf.py +239 -0
- mobile_agent/verify/proof.py +192 -0
- mobile_agent/verify/replay_admission.py +48 -0
- mobile_agent/verify/report.py +952 -0
- mobile_agent/verify/run_cleanup.py +67 -0
- mobile_agent/verify/run_links.py +48 -0
- mobile_agent/verify/run_perf.py +44 -0
- mobile_agent/verify/run_secrets.py +39 -0
- mobile_agent/verify/runner.py +976 -0
- mobile_agent/verify/runs.py +57 -0
- mobile_agent/verify/screen_coverage.py +181 -0
- mobile_agent/verify/smart.py +106 -0
- mobile_agent/verify/snapshot_run.py +114 -0
- mobile_agent/verify/snapshots.py +306 -0
- mobile_agent/verify/sound.py +335 -0
- mobile_agent/verify/source_observation.py +130 -0
- mobile_agent/verify/system_surfaces/__init__.py +7 -0
- mobile_agent/verify/system_surfaces/play.py +106 -0
- mobile_agent/verify/system_surfaces/spec.py +139 -0
- mobile_agent/verify/system_surfaces/stage.py +138 -0
- mobile_agent/verify/system_surfaces/surfaces.py +121 -0
- mobile_agent/verify/tree.py +223 -0
- mobile_agent/verify/verdict.py +377 -0
- mobile_agent/verify/wire.py +392 -0
- mobile_agent/verify/wirestore.py +176 -0
- mobile_agent/waiting/__init__.py +1 -0
- mobile_agent/waiting/contacts.py +73 -0
- mobile_agent/waiting/drafts.py +136 -0
- mobile_agent/waiting/job.py +145 -0
- mobile_agent/waiting/messages.py +174 -0
- mobile_agent/waiting/rank.py +115 -0
- mobile_agent/waiting/register.py +26 -0
- mobile_agent/watch.py +502 -0
- mobile_agent/wda/__init__.py +2 -0
- mobile_agent/wda/frame_clock.py +646 -0
- mobile_agent/wda/frame_source.py +325 -0
- mobile_agent/wda/frame_store.py +115 -0
- mobile_agent/wda/gate.py +163 -0
- mobile_agent/wda/gestures.py +291 -0
- mobile_agent/wda/guards.py +94 -0
- mobile_agent/wda/identity.py +55 -0
- mobile_agent/wda/observe.py +884 -0
- mobile_agent/wda/patches.py +856 -0
- mobile_agent/wda/point_gestures.py +150 -0
- mobile_agent/wda/source.py +32 -0
- mobile_agent/wda/system_dialogs.py +571 -0
- mobile_agent/wda/text.py +694 -0
- mobile_agent/wda/timeouts.py +77 -0
- mobile_agent/wda/video.py +505 -0
- mobile_agent/wda/watchdog.py +243 -0
- mobile_agent/wireless/__init__.py +15 -0
- mobile_agent/wireless/api.py +354 -0
- mobile_agent/wireless/cli.py +287 -0
- mobile_agent/wireless/devicectl.py +229 -0
- mobile_agent/wireless/exposure.py +33 -0
- mobile_agent/wireless/mobster-wifi-pair.c +122 -0
- mobile_agent/wireless/monitor.py +332 -0
- mobile_agent/wireless/network.py +334 -0
- mobile_agent/wireless/onboarding.py +293 -0
- mobile_agent/wireless/pair.py +124 -0
- mobile_agent/wireless/probe.py +322 -0
- mobile_agent/wireless/register.py +8 -0
- mobile_agent/wireless/relay.py +179 -0
- mobile_agent/wireless/result.py +87 -0
- mobile_agent/wireless/store.py +85 -0
- mobile_agent/wireless/switch.py +77 -0
- mobile_agent/wireless/transport.py +118 -0
- mobile_agent/wireless/words.py +81 -0
- mobile_agent/wireless/xctestrun.py +83 -0
- mobile_agent/workflow_tools.py +59 -0
- mobile_agent/workflows/__init__.py +2 -0
- mobile_agent/workflows/brief.py +201 -0
- mobile_agent/workflows/cron.py +110 -0
- mobile_agent/workflows/health.py +257 -0
- mobile_agent/workflows/history.py +126 -0
- mobile_agent/workflows/records.py +442 -0
- mobile_agent/workflows/runs.py +1000 -0
- mobile_agent/workflows/scheduler.py +134 -0
- mobile_agent/workflows/service.py +973 -0
- mobile_agent/workflows/starters.py +285 -0
- mobile_agent/workflows/triggers.py +248 -0
- mobile_agent/xray/__init__.py +1 -0
- mobile_agent/xray/app_cli.py +116 -0
- mobile_agent/xray/cli.py +21 -0
- mobile_agent/xray/crawl.py +754 -0
- mobile_agent/xray/diff.py +171 -0
- mobile_agent/xray/diff_cli.py +43 -0
- mobile_agent/xray/job.py +184 -0
- mobile_agent/xray/paywall.py +188 -0
- mobile_agent/xray/policy.py +305 -0
- mobile_agent/xray/register.py +43 -0
- mobile_agent/xray/report.py +283 -0
- mobile_agent/xray/screens.py +117 -0
- mobster_cli-0.4.3.dist-info/METADATA +93 -0
- mobster_cli-0.4.3.dist-info/RECORD +1009 -0
- mobster_cli-0.4.3.dist-info/WHEEL +5 -0
- mobster_cli-0.4.3.dist-info/entry_points.txt +3 -0
- mobster_cli-0.4.3.dist-info/licenses/LICENSE +21 -0
- mobster_cli-0.4.3.dist-info/top_level.txt +1 -0
mobile_agent/devices.py
ADDED
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
"""Every phone Mobster can drive, their ports, and naming one (SPEC §3, docs/devices.md)."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import copy
|
|
5
|
+
import errno
|
|
6
|
+
import fcntl
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import socket
|
|
11
|
+
import stat
|
|
12
|
+
import tempfile
|
|
13
|
+
import threading
|
|
14
|
+
import time
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from urllib.parse import urlsplit
|
|
17
|
+
|
|
18
|
+
STATES = ("ready", "busy", "connected", "needs_setup", "disconnected")
|
|
19
|
+
STORE = "devices.json"
|
|
20
|
+
STORE_LOCK = "devices.lock"
|
|
21
|
+
LOCK_TIMEOUT = 10.0
|
|
22
|
+
WDA_BASE, MJPEG_BASE = 8100, 9100
|
|
23
|
+
FIRST_SLOT, LAST_SLOT = 1, 99
|
|
24
|
+
# The relay and WDA listen here only. Without it iproxy listened on every interface
|
|
25
|
+
# of the Mac (lsof: *:8100) and WDA on the phone's Wi-Fi address; neither has auth.
|
|
26
|
+
LOOPBACK = "127.0.0.1"
|
|
27
|
+
ID_PATTERN = re.compile(r"[A-Za-z0-9._-]{1,64}")
|
|
28
|
+
UDID_PATTERN = re.compile(r"[0-9A-Fa-f-]{24,40}")
|
|
29
|
+
|
|
30
|
+
# The one sentence each state says, when nothing more precise is known.
|
|
31
|
+
REASONS = {
|
|
32
|
+
# Plain words: the app shows these under a phone's name (dashboard/src/lib/devices.ts), and the CLI's
|
|
33
|
+
# `mobster devices` prints them. "Mobster's helper" is the on-phone runner (WebDriverAgent).
|
|
34
|
+
"busy": "A task is running on it.",
|
|
35
|
+
"busy_elsewhere": "Another Mobster app or command is using it.",
|
|
36
|
+
"untrusted": "Unlock it and tap Trust on it.",
|
|
37
|
+
"not_built": "Set it up once. Mobster walks you through it.",
|
|
38
|
+
"built_for_other": "Set it up again: Mobster's helper on it was made for another iPhone.",
|
|
39
|
+
# Both are refreshed by themselves while the phone is plugged in and unlocked (runner_renewal.py).
|
|
40
|
+
"out_of_date": "It needs a quick refresh after Mobster's update. Mobster does it by itself while it's plugged in "
|
|
41
|
+
"and unlocked.",
|
|
42
|
+
"expired": "It needs a quick refresh. Free Apple accounts ask for this every 7 days. Mobster does it by itself "
|
|
43
|
+
"while it's plugged in and unlocked.",
|
|
44
|
+
"starting": "Mobster's helper on it isn't answering yet. Keep it unlocked and connected.",
|
|
45
|
+
# iOS waits for automation mode (runner_hints.AUTOMATION_WAIT_S): the switch to turn on, said while it waits.
|
|
46
|
+
"ui_automation": "Turn on Settings › Developer › Enable UI Automation on it. If it asks for its passcode, enter "
|
|
47
|
+
"it. Mobster's helper starts by itself after that.",
|
|
48
|
+
"stopped": "Mobster's helper isn't running on it. Start it again to see the screen.",
|
|
49
|
+
# Its helper's first start (device_manager.prearm): the switch iOS asks about, said before the first launch.
|
|
50
|
+
"prearm": "Its helper is starting for the first time. Open Settings › Developer on it and turn on Enable UI "
|
|
51
|
+
"Automation, so it starts on the first try.",
|
|
52
|
+
"unplugged": "It's unplugged. Plug it into this Mac with a cable.",
|
|
53
|
+
"sim_off": "The simulator is shut down. Start it to use it.",
|
|
54
|
+
"sim_no_wda": "The simulator is on, but Mobster's helper isn't running on it. Start it to use it.",
|
|
55
|
+
"wda_down": "Nothing answers at its address.",
|
|
56
|
+
"locked": "Unlock it to run tasks and see its screen.",
|
|
57
|
+
# Only with Wi-Fi for iPhones on (MOBSTER_WIFI_TRANSPORT) and Wi-Fi turned on for the phone (wireless/).
|
|
58
|
+
"wifi_unreachable": "It isn't reachable over Wi-Fi. Keep it unlocked and on the same Wi-Fi as this Mac, or plug "
|
|
59
|
+
"it in.",
|
|
60
|
+
# Disconnected in Settings › Phones: still known, not used until Connect.
|
|
61
|
+
"paused": "Disconnected in Mobster. Connect it to use it again.",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# A phone's own settings in Mobster (devices.json "prefs", by device id): a nickname shown instead of its name, whether
|
|
65
|
+
# a task on it asks before acting (None: the Mac-wide Safety setting), and the apps a task on it may use (None: all).
|
|
66
|
+
NICKNAME_MAX = 64
|
|
67
|
+
PREF_APPS_MAX = 64
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class DeviceNotFound(LookupError):
|
|
71
|
+
"""No device has that id, UDID or name."""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class AmbiguousDevice(LookupError):
|
|
75
|
+
"""More than one device has that name."""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def wda_url(port):
|
|
79
|
+
return f"http://{LOOPBACK}:{int(port)}"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def port_of(url):
|
|
83
|
+
try:
|
|
84
|
+
return urlsplit(url or "").port
|
|
85
|
+
except ValueError:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def slot_ports(slot):
|
|
90
|
+
"""(WDA port, MJPEG port) of a USB slot: 8100+N and 9100+N."""
|
|
91
|
+
return WDA_BASE + slot, MJPEG_BASE + slot
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def listening(port, host=LOOPBACK, timeout=.2):
|
|
95
|
+
"""Whether something accepts connections on this Mac's loopback at ``port``."""
|
|
96
|
+
try:
|
|
97
|
+
with socket.create_connection((host, int(port)), timeout=timeout):
|
|
98
|
+
return True
|
|
99
|
+
except OSError:
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def allocate_slot(entries, busy=None):
|
|
104
|
+
busy = busy or listening
|
|
105
|
+
taken = {int(entry["slot"]) for entry in entries if isinstance(entry.get("slot"), int)}
|
|
106
|
+
for slot in range(FIRST_SLOT, LAST_SLOT + 1):
|
|
107
|
+
if slot in taken:
|
|
108
|
+
continue
|
|
109
|
+
wda, mjpeg = slot_ports(slot)
|
|
110
|
+
if busy(wda) or busy(mjpeg):
|
|
111
|
+
continue # another program's port: skipped, never taken over
|
|
112
|
+
return slot
|
|
113
|
+
raise LookupError(f"Every port from {WDA_BASE + FIRST_SLOT} to {WDA_BASE + LAST_SLOT} is taken, so Mobster "
|
|
114
|
+
"has no room for another iPhone. Forget a phone you no longer use.")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def device_id_for_url(url):
|
|
118
|
+
"""The id of a device known only by its WDA address: wda-<port>."""
|
|
119
|
+
port = port_of(url)
|
|
120
|
+
return f"wda-{port}" if port else "wda"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# -- devices.json --------------------------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
MAX_CHOSEN = 100 # the composer's phones for one task on each (fanout's own limits apply when it runs)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def clean_chosen(items):
|
|
129
|
+
"""Device ids in order, each once, at most MAX_CHOSEN; anything else is dropped."""
|
|
130
|
+
out = []
|
|
131
|
+
for item in items:
|
|
132
|
+
if isinstance(item, str) and ID_PATTERN.fullmatch(item) and item not in out:
|
|
133
|
+
out.append(item)
|
|
134
|
+
return out[:MAX_CHOSEN]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class DeviceStore:
|
|
138
|
+
"""devices.json in the data folder: the phones set up besides the primary, their slots, and the device used last."""
|
|
139
|
+
|
|
140
|
+
def __init__(self, data_dir):
|
|
141
|
+
self.data_dir = Path(data_dir) if data_dir is not None else None
|
|
142
|
+
self.path = self.data_dir / STORE if self.data_dir is not None else None
|
|
143
|
+
self.lock_path = self.data_dir / STORE_LOCK if self.data_dir is not None else None
|
|
144
|
+
self.memory = self._empty() if self.data_dir is None else None
|
|
145
|
+
self.memory_lock = threading.Lock()
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def _empty():
|
|
149
|
+
return {"version": 1, "phones": [], "lastUsed": None, "defaultDevice": None, "disconnected": [],
|
|
150
|
+
"prefs": {}, "chosen": []}
|
|
151
|
+
|
|
152
|
+
def read(self):
|
|
153
|
+
if self.data_dir is None:
|
|
154
|
+
with self.memory_lock:
|
|
155
|
+
return copy.deepcopy(self.memory)
|
|
156
|
+
try:
|
|
157
|
+
data = json.loads(self.path.read_text())
|
|
158
|
+
except (OSError, ValueError):
|
|
159
|
+
data = None
|
|
160
|
+
if not isinstance(data, dict): # damaged: it holds no secret and nothing that can't be set up again
|
|
161
|
+
return self._empty()
|
|
162
|
+
entries = data.get("phones")
|
|
163
|
+
if not isinstance(entries, list):
|
|
164
|
+
entries = []
|
|
165
|
+
phones = [entry for entry in entries if isinstance(entry, dict)
|
|
166
|
+
and isinstance(entry.get("udid"), str) and UDID_PATTERN.fullmatch(entry["udid"])
|
|
167
|
+
and type(entry.get("slot")) is int and FIRST_SLOT <= entry["slot"] <= LAST_SLOT
|
|
168
|
+
and type(entry.get("wdaPort")) is int and type(entry.get("mjpegPort")) is int
|
|
169
|
+
and (entry["wdaPort"], entry["mjpegPort"]) == slot_ports(entry["slot"])]
|
|
170
|
+
last, default = data.get("lastUsed"), data.get("defaultDevice")
|
|
171
|
+
paused = data.get("disconnected")
|
|
172
|
+
prefs = data.get("prefs") if isinstance(data.get("prefs"), dict) else {}
|
|
173
|
+
chosen = data.get("chosen")
|
|
174
|
+
return {"version": 1, "phones": phones,
|
|
175
|
+
"lastUsed": last if isinstance(last, str) and ID_PATTERN.fullmatch(last) else None,
|
|
176
|
+
"defaultDevice": default if isinstance(default, str) and ID_PATTERN.fullmatch(default) else None,
|
|
177
|
+
"disconnected": sorted({item for item in paused if isinstance(item, str)
|
|
178
|
+
and ID_PATTERN.fullmatch(item)})
|
|
179
|
+
if isinstance(paused, list) else [],
|
|
180
|
+
"prefs": {key: clean for key, value in prefs.items() if isinstance(key, str)
|
|
181
|
+
and ID_PATTERN.fullmatch(key)
|
|
182
|
+
for clean in [clean_prefs(value)] if clean},
|
|
183
|
+
"chosen": clean_chosen(chosen) if isinstance(chosen, list) else []}
|
|
184
|
+
|
|
185
|
+
def read_strict(self):
|
|
186
|
+
"""Fresh policy for trusted suite admission; a damaged saved rule never becomes allow-all."""
|
|
187
|
+
if self.data_dir is None:
|
|
188
|
+
with self.memory_lock:
|
|
189
|
+
return copy.deepcopy(self.memory)
|
|
190
|
+
from .transport import decode_json
|
|
191
|
+
try:
|
|
192
|
+
fd = os.open(self.path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
|
|
193
|
+
except FileNotFoundError:
|
|
194
|
+
return self._empty() # A new Mac has no device preferences yet.
|
|
195
|
+
except OSError:
|
|
196
|
+
raise ValueError("Saved device rules cannot be read") from None
|
|
197
|
+
try:
|
|
198
|
+
with os.fdopen(fd, "rb") as stream:
|
|
199
|
+
info = os.fstat(stream.fileno())
|
|
200
|
+
if not stat.S_ISREG(info.st_mode) or info.st_size > 1_000_000:
|
|
201
|
+
raise ValueError("Saved device rules are not a bounded regular file")
|
|
202
|
+
data = decode_json(stream.read(1_000_001))
|
|
203
|
+
except (OSError, UnicodeError, ValueError, RecursionError):
|
|
204
|
+
raise ValueError("Saved device rules are damaged or unreadable") from None
|
|
205
|
+
if (not isinstance(data, dict) or type(data.get("version")) is not int or data["version"] != 1 or
|
|
206
|
+
set(data) != {"version", "phones", "lastUsed", "defaultDevice", "disconnected", "prefs", "chosen"} or
|
|
207
|
+
not isinstance(data["phones"], list) or not isinstance(data["disconnected"], list) or
|
|
208
|
+
not isinstance(data["prefs"], dict) or not isinstance(data["chosen"], list)):
|
|
209
|
+
raise ValueError("Saved device rules are damaged")
|
|
210
|
+
if any(not isinstance(entry, dict) or not isinstance(entry.get("udid"), str) or
|
|
211
|
+
not UDID_PATTERN.fullmatch(entry["udid"]) or type(entry.get("slot")) is not int or
|
|
212
|
+
not FIRST_SLOT <= entry["slot"] <= LAST_SLOT or
|
|
213
|
+
(entry.get("wdaPort"), entry.get("mjpegPort")) != slot_ports(entry["slot"])
|
|
214
|
+
for entry in data["phones"]):
|
|
215
|
+
raise ValueError("Saved device rules are damaged")
|
|
216
|
+
ids = data["disconnected"] + data["chosen"]
|
|
217
|
+
if (any(not isinstance(item, str) or not ID_PATTERN.fullmatch(item) for item in ids) or
|
|
218
|
+
any(value is not None and (not isinstance(value, str) or not ID_PATTERN.fullmatch(value))
|
|
219
|
+
for value in (data["lastUsed"], data["defaultDevice"])) or
|
|
220
|
+
len(set(data["chosen"])) != len(data["chosen"]) or
|
|
221
|
+
len(clean_chosen(data["chosen"])) != len(data["chosen"])):
|
|
222
|
+
raise ValueError("Saved device rules are damaged")
|
|
223
|
+
for key, value in data["prefs"].items():
|
|
224
|
+
if not ID_PATTERN.fullmatch(key) or not isinstance(value, dict) or set(value) - {
|
|
225
|
+
"nickname", "askBeforeActing", "apps"}:
|
|
226
|
+
raise ValueError("Saved device rules are damaged")
|
|
227
|
+
clean_prefs(value, strict=True)
|
|
228
|
+
return data
|
|
229
|
+
|
|
230
|
+
def phones(self):
|
|
231
|
+
return self.read()["phones"]
|
|
232
|
+
|
|
233
|
+
@contextlib.contextmanager
|
|
234
|
+
def edit(self):
|
|
235
|
+
if self.data_dir is None:
|
|
236
|
+
with self.memory_lock:
|
|
237
|
+
yield self.memory
|
|
238
|
+
return
|
|
239
|
+
self.data_dir.mkdir(parents=True, exist_ok=True)
|
|
240
|
+
fd = os.open(self.lock_path, os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
|
|
241
|
+
try:
|
|
242
|
+
end = time.monotonic() + LOCK_TIMEOUT
|
|
243
|
+
while True:
|
|
244
|
+
try:
|
|
245
|
+
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
246
|
+
break
|
|
247
|
+
except OSError as exc:
|
|
248
|
+
if exc.errno not in (errno.EWOULDBLOCK, errno.EAGAIN) or time.monotonic() >= end:
|
|
249
|
+
raise OSError("Another Mobster process is holding the device list") from None
|
|
250
|
+
time.sleep(.05)
|
|
251
|
+
data = self.read()
|
|
252
|
+
before = json.dumps(data, sort_keys=True)
|
|
253
|
+
yield data
|
|
254
|
+
if json.dumps(data, sort_keys=True) != before:
|
|
255
|
+
self._write(data)
|
|
256
|
+
finally:
|
|
257
|
+
os.close(fd)
|
|
258
|
+
|
|
259
|
+
def _write(self, data):
|
|
260
|
+
fd, temporary = tempfile.mkstemp(prefix=".devices.", suffix=".json", dir=self.data_dir)
|
|
261
|
+
try:
|
|
262
|
+
with os.fdopen(fd, "w", encoding="utf-8") as stream:
|
|
263
|
+
os.fchmod(stream.fileno(), 0o600)
|
|
264
|
+
stream.write(json.dumps(data, indent=1) + "\n")
|
|
265
|
+
os.replace(temporary, self.path)
|
|
266
|
+
except BaseException:
|
|
267
|
+
with contextlib.suppress(OSError):
|
|
268
|
+
os.unlink(temporary)
|
|
269
|
+
raise
|
|
270
|
+
|
|
271
|
+
def add(self, udid, name=None, busy=None):
|
|
272
|
+
"""The phone's entry, allocating its slot the first time."""
|
|
273
|
+
with self.edit() as data:
|
|
274
|
+
entry = next((item for item in data["phones"] if item["udid"] == udid), None)
|
|
275
|
+
if entry is None:
|
|
276
|
+
slot = allocate_slot(data["phones"], busy)
|
|
277
|
+
wda, mjpeg = slot_ports(slot)
|
|
278
|
+
entry = {"udid": udid, "name": name, "slot": slot, "wdaPort": wda, "mjpegPort": mjpeg,
|
|
279
|
+
"addedAt": round(time.time() * 1000)}
|
|
280
|
+
data["phones"].append(entry)
|
|
281
|
+
elif name and entry.get("name") != name:
|
|
282
|
+
entry["name"] = name
|
|
283
|
+
return dict(entry)
|
|
284
|
+
|
|
285
|
+
def remove(self, udid):
|
|
286
|
+
with self.edit() as data:
|
|
287
|
+
data["phones"] = [item for item in data["phones"] if item["udid"] != udid]
|
|
288
|
+
for key in ("lastUsed", "defaultDevice"):
|
|
289
|
+
if data.get(key) == udid:
|
|
290
|
+
data[key] = None
|
|
291
|
+
data["disconnected"] = [item for item in data["disconnected"] if item != udid]
|
|
292
|
+
data["prefs"].pop(udid, None)
|
|
293
|
+
data["chosen"] = [item for item in data["chosen"] if item != udid]
|
|
294
|
+
|
|
295
|
+
def set_default(self, device_id):
|
|
296
|
+
"""The phone a new task runs on unless another is picked (None: no choice, Fleet.default decides)."""
|
|
297
|
+
if device_id is not None and not ID_PATTERN.fullmatch(str(device_id)):
|
|
298
|
+
raise ValueError("A device id is 1 to 64 letters, digits, dots, dashes or underscores")
|
|
299
|
+
with self.edit() as data:
|
|
300
|
+
data["defaultDevice"] = device_id
|
|
301
|
+
|
|
302
|
+
def set_chosen(self, device_ids):
|
|
303
|
+
if not isinstance(device_ids, (list, tuple)) or len(device_ids) > MAX_CHOSEN or not all(
|
|
304
|
+
isinstance(item, str) and ID_PATTERN.fullmatch(item) for item in device_ids) or len(
|
|
305
|
+
set(device_ids)) != len(device_ids):
|
|
306
|
+
raise ValueError(f"Choose at most {MAX_CHOSEN} different phones by their ids")
|
|
307
|
+
with self.edit() as data:
|
|
308
|
+
data["chosen"] = clean_chosen(device_ids)
|
|
309
|
+
|
|
310
|
+
def set_disconnected(self, device_id, value):
|
|
311
|
+
if not ID_PATTERN.fullmatch(str(device_id)):
|
|
312
|
+
raise ValueError("A device id is 1 to 64 letters, digits, dots, dashes or underscores")
|
|
313
|
+
with self.edit() as data:
|
|
314
|
+
others = [item for item in data["disconnected"] if item != device_id]
|
|
315
|
+
data["disconnected"] = sorted(others + [device_id]) if value else others
|
|
316
|
+
|
|
317
|
+
def prefs(self, device_id):
|
|
318
|
+
return dict(self.read()["prefs"].get(device_id) or {})
|
|
319
|
+
|
|
320
|
+
def set_prefs(self, device_id, **changes):
|
|
321
|
+
"""Change a phone's own settings (``clean_prefs``); a value of None removes it. The phone's settings after."""
|
|
322
|
+
if not ID_PATTERN.fullmatch(str(device_id)):
|
|
323
|
+
raise ValueError("A device id is 1 to 64 letters, digits, dots, dashes or underscores")
|
|
324
|
+
unknown = set(changes) - {"nickname", "askBeforeActing", "apps"}
|
|
325
|
+
if unknown:
|
|
326
|
+
raise ValueError("A phone's settings are nickname, askBeforeActing and apps")
|
|
327
|
+
with self.edit() as data:
|
|
328
|
+
current = dict(data["prefs"].get(device_id) or {})
|
|
329
|
+
for key, value in changes.items():
|
|
330
|
+
if value is None:
|
|
331
|
+
current.pop(key, None)
|
|
332
|
+
else:
|
|
333
|
+
current[key] = value
|
|
334
|
+
clean = clean_prefs(current, strict=True)
|
|
335
|
+
if clean:
|
|
336
|
+
data["prefs"][device_id] = clean
|
|
337
|
+
else:
|
|
338
|
+
data["prefs"].pop(device_id, None)
|
|
339
|
+
return dict(clean)
|
|
340
|
+
|
|
341
|
+
def set_last_used(self, device_id):
|
|
342
|
+
with self.edit() as data:
|
|
343
|
+
data["lastUsed"] = device_id
|
|
344
|
+
|
|
345
|
+
def set_wifi(self, udid, value):
|
|
346
|
+
"""Set (a dict) or clear (None) the phone's ``wifi`` (wireless/store.py). LookupError when it has no entry."""
|
|
347
|
+
with self.edit() as data:
|
|
348
|
+
entry = next((item for item in data["phones"] if item["udid"].upper() == str(udid).upper()), None)
|
|
349
|
+
if entry is None:
|
|
350
|
+
raise LookupError("not_set_up")
|
|
351
|
+
if value is None:
|
|
352
|
+
entry.pop("wifi", None)
|
|
353
|
+
else:
|
|
354
|
+
entry["wifi"] = dict(value)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def clean_nickname(value):
|
|
358
|
+
"""A nickname as kept: its words joined by single spaces, 1 to 64 characters, no control characters."""
|
|
359
|
+
if not isinstance(value, str):
|
|
360
|
+
raise ValueError("A name is text")
|
|
361
|
+
text = " ".join(value.split())
|
|
362
|
+
if not 1 <= len(text) <= NICKNAME_MAX or any(ord(char) < 32 or 0x7f <= ord(char) < 0xa0 for char in text):
|
|
363
|
+
raise ValueError(f"A name is 1 to {NICKNAME_MAX} characters")
|
|
364
|
+
return text
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def clean_prefs(value, strict=False):
|
|
368
|
+
from .state import validate_bundle_id
|
|
369
|
+
if not isinstance(value, dict):
|
|
370
|
+
if strict:
|
|
371
|
+
raise ValueError("A phone's settings are an object")
|
|
372
|
+
return {}
|
|
373
|
+
out = {}
|
|
374
|
+
if value.get("nickname") is not None:
|
|
375
|
+
try:
|
|
376
|
+
out["nickname"] = clean_nickname(value["nickname"])
|
|
377
|
+
except ValueError:
|
|
378
|
+
if strict:
|
|
379
|
+
raise
|
|
380
|
+
if value.get("askBeforeActing") is not None:
|
|
381
|
+
if type(value["askBeforeActing"]) is bool:
|
|
382
|
+
out["askBeforeActing"] = value["askBeforeActing"]
|
|
383
|
+
elif strict:
|
|
384
|
+
raise ValueError("askBeforeActing is true, false or null (the Mac-wide setting)")
|
|
385
|
+
if value.get("apps") is not None:
|
|
386
|
+
apps = value["apps"]
|
|
387
|
+
try:
|
|
388
|
+
if not isinstance(apps, list) or not 1 <= len(apps) <= PREF_APPS_MAX:
|
|
389
|
+
raise ValueError
|
|
390
|
+
out["apps"] = sorted({validate_bundle_id(bundle) for bundle in apps})
|
|
391
|
+
except (ValueError, TypeError):
|
|
392
|
+
if strict:
|
|
393
|
+
raise ValueError(f"apps is null (every app) or 1 to {PREF_APPS_MAX} bundle identifiers") from None
|
|
394
|
+
return out
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def phone_folder(data_dir, udid):
|
|
398
|
+
"""Where a phone other than the primary keeps its runner build, settings and logs."""
|
|
399
|
+
return Path(data_dir) / "devices" / udid
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
# -- reading the machine ---------------------------------------------------------------------------------------
|
|
403
|
+
|
|
404
|
+
def record(*, id, kind, name, wda_url_value, udid=None, model=None, model_name=None, ios=None, state="disconnected",
|
|
405
|
+
reason=None, mjpeg_url=None, primary=False, set_up=True, enclosure=None):
|
|
406
|
+
if state not in STATES:
|
|
407
|
+
raise ValueError(f"unknown device state {state}")
|
|
408
|
+
return {"id": id, "kind": kind, "name": name or ("iPhone" if kind == "usb" else id), "udid": udid,
|
|
409
|
+
"model": model, "modelName": model_name, "ios": ios, "enclosure": enclosure, "state": state,
|
|
410
|
+
"reason": None if state == "ready" else (reason or None), "primary": bool(primary),
|
|
411
|
+
"setUp": bool(set_up), "wdaUrl": wda_url_value, "mjpegUrl": mjpeg_url}
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def usb_state(device, manager, wda_ready, lease_free, locked=None):
|
|
415
|
+
if device is None:
|
|
416
|
+
return "disconnected", REASONS["unplugged"]
|
|
417
|
+
if not device.get("trusted"):
|
|
418
|
+
return "needs_setup", REASONS["untrusted"]
|
|
419
|
+
if manager is None:
|
|
420
|
+
return "needs_setup", REASONS["not_built"]
|
|
421
|
+
settings = manager.settings()
|
|
422
|
+
built_for = settings.get("built_for", settings.get("udid"))
|
|
423
|
+
if manager.expired():
|
|
424
|
+
return "needs_setup", REASONS["expired"]
|
|
425
|
+
if not manager.built():
|
|
426
|
+
if built_for and built_for != device.get("udid"):
|
|
427
|
+
return "needs_setup", REASONS["built_for_other"]
|
|
428
|
+
if manager.out_of_date():
|
|
429
|
+
return "needs_setup", REASONS["out_of_date"]
|
|
430
|
+
return "needs_setup", REASONS["not_built"]
|
|
431
|
+
if not wda_ready():
|
|
432
|
+
waiting = getattr(manager, "runner_waiting", None)
|
|
433
|
+
if callable(waiting) and waiting() == "unlock":
|
|
434
|
+
return "connected", REASONS["locked"] # its helper starts when the phone is unlocked
|
|
435
|
+
running = manager.runner is not None and manager.runner.running
|
|
436
|
+
if running and getattr(manager, "runner_cause", None) == "ui_automation_off":
|
|
437
|
+
return "connected", REASONS["ui_automation"]
|
|
438
|
+
prearm = getattr(manager, "prearm", None)
|
|
439
|
+
if running and callable(prearm) and prearm():
|
|
440
|
+
return "connected", REASONS["prearm"]
|
|
441
|
+
return "connected", REASONS["starting"] if running else REASONS["stopped"]
|
|
442
|
+
if not lease_free():
|
|
443
|
+
return "busy", REASONS["busy_elsewhere"]
|
|
444
|
+
if locked:
|
|
445
|
+
return "connected", REASONS["locked"]
|
|
446
|
+
return "ready", None
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def wda_answers(url, timeout=1.0):
|
|
450
|
+
"""WDA's /status says ready, asked under an absolute deadline (a phone that drips bytes can't extend it)."""
|
|
451
|
+
from .transport import read_kept
|
|
452
|
+
try:
|
|
453
|
+
return read_kept(url, "/status", timeout=timeout).get("value", {}).get("ready") is True
|
|
454
|
+
except Exception:
|
|
455
|
+
return False
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def lease_free(url, udid=None):
|
|
459
|
+
from .journal import JournalError, Lease, LeaseHeld
|
|
460
|
+
try:
|
|
461
|
+
lease = Lease.device(url, wait=0, udid=udid)
|
|
462
|
+
except LeaseHeld:
|
|
463
|
+
return False
|
|
464
|
+
except JournalError:
|
|
465
|
+
return True # an unusable lease folder guards nothing; the run says so when it starts
|
|
466
|
+
lease.close()
|
|
467
|
+
return True
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def simulator_folder():
|
|
471
|
+
from .paths import dev_data_dir
|
|
472
|
+
return dev_data_dir()
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def simulator_rows(data_dir=None):
|
|
476
|
+
try:
|
|
477
|
+
from .sim.registry import Registry
|
|
478
|
+
except ImportError:
|
|
479
|
+
return []
|
|
480
|
+
folder = Path(data_dir) if data_dir else simulator_folder()
|
|
481
|
+
if not (folder / "simulators.json").exists():
|
|
482
|
+
return [] # never runs simctl on a Mac with no Mobster simulator
|
|
483
|
+
try:
|
|
484
|
+
entries = Registry(folder).read()
|
|
485
|
+
except Exception:
|
|
486
|
+
return []
|
|
487
|
+
states = {}
|
|
488
|
+
try:
|
|
489
|
+
from .sim import SimulatorManager
|
|
490
|
+
states = {item.udid: item.state for item in SimulatorManager(data_dir=folder)._devices()}
|
|
491
|
+
except Exception:
|
|
492
|
+
pass
|
|
493
|
+
return [{"udid": entry["udid"], "name": entry.get("name"), "device_type": entry.get("device_type"),
|
|
494
|
+
"runtime": entry.get("runtime"), "state": states.get(entry["udid"], "Missing" if states else "Unknown"),
|
|
495
|
+
"wda_url": wda_url(entry["wda_port"]), "mjpeg_url": wda_url(entry["mjpeg_port"])}
|
|
496
|
+
for entry in entries if entry.get("wda_port") and entry.get("mjpeg_port")]
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def simulator_record(row, *, ready=None, free=None, busy_here=False):
|
|
500
|
+
"""One simulator row (simulator_rows) as a device. ``ready``/``free`` default to probing its WDA and lease."""
|
|
501
|
+
booted = row.get("state") == "Booted"
|
|
502
|
+
if busy_here:
|
|
503
|
+
state, reason = "busy", REASONS["busy"]
|
|
504
|
+
elif not booted:
|
|
505
|
+
state, reason = "disconnected", REASONS["sim_off"]
|
|
506
|
+
elif not (wda_answers(row["wda_url"]) if ready is None else ready):
|
|
507
|
+
state, reason = "connected", REASONS["sim_no_wda"]
|
|
508
|
+
elif not (lease_free(row["wda_url"]) if free is None else free):
|
|
509
|
+
state, reason = "busy", REASONS["busy_elsewhere"]
|
|
510
|
+
else:
|
|
511
|
+
state, reason = "ready", None
|
|
512
|
+
runtime = str(row.get("runtime") or "")
|
|
513
|
+
return record(id=row["udid"], kind="simulator", name=row.get("name"), udid=row["udid"],
|
|
514
|
+
model=row.get("device_type"), model_name=row.get("device_type"),
|
|
515
|
+
ios=runtime.removeprefix("iOS ").strip() or None, state=state, reason=reason,
|
|
516
|
+
wda_url_value=row["wda_url"], mjpeg_url=row.get("mjpeg_url"))
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def simulator_records(probe=True):
|
|
520
|
+
return [simulator_record(row, **({} if probe else {"ready": True, "free": True})) for row in simulator_rows()]
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def static_specs(config=None):
|
|
524
|
+
"""WDA devices given by address: ``MOBSTER_WDA_DEVICES`` (pool.wda_specs_from_env) and ``config.devices``."""
|
|
525
|
+
from .pool import wda_specs_from_config
|
|
526
|
+
try:
|
|
527
|
+
return wda_specs_from_config(config)
|
|
528
|
+
except ValueError:
|
|
529
|
+
return []
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def static_record(url, *, id=None, name=None, mjpeg_url=None, ready=None, free=None):
|
|
533
|
+
ready = wda_answers(url) if ready is None else ready
|
|
534
|
+
if not ready:
|
|
535
|
+
state, reason = "disconnected", REASONS["wda_down"]
|
|
536
|
+
elif not (lease_free(url) if free is None else free):
|
|
537
|
+
state, reason = "busy", REASONS["busy_elsewhere"]
|
|
538
|
+
else:
|
|
539
|
+
state, reason = "ready", None
|
|
540
|
+
return record(id=id or device_id_for_url(url), kind="wda", name=name or f"WebDriverAgent on port {port_of(url)}",
|
|
541
|
+
state=state, reason=reason, wda_url_value=url.rstrip("/"), mjpeg_url=mjpeg_url)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def discover(data_dir=None, *, sims=True, extra_urls=(), probe=True):
|
|
545
|
+
from .device_info import MODEL_NAMES
|
|
546
|
+
from .device_manager import DeviceManager
|
|
547
|
+
from .mac_tools import WDA_PORT
|
|
548
|
+
from .paths import user_data_dir
|
|
549
|
+
data_dir = Path(data_dir) if data_dir else user_data_dir()
|
|
550
|
+
records, seen_urls = [], set()
|
|
551
|
+
|
|
552
|
+
def add(item):
|
|
553
|
+
identity = (item.get("wdaUrl") or "").rstrip("/")
|
|
554
|
+
if identity and identity in seen_urls:
|
|
555
|
+
return
|
|
556
|
+
if identity:
|
|
557
|
+
seen_urls.add(identity)
|
|
558
|
+
records.append(item)
|
|
559
|
+
|
|
560
|
+
primary = DeviceManager(data_dir, wda_url(WDA_PORT))
|
|
561
|
+
store = DeviceStore(data_dir)
|
|
562
|
+
extras = store.phones()
|
|
563
|
+
claimed = {entry["udid"] for entry in extras}
|
|
564
|
+
primary.claimed = lambda: claimed
|
|
565
|
+
try:
|
|
566
|
+
connected = primary.devices()
|
|
567
|
+
except Exception:
|
|
568
|
+
connected = []
|
|
569
|
+
by_udid = {item["udid"]: item for item in connected}
|
|
570
|
+
wifi = wifi_phones(data_dir)
|
|
571
|
+
|
|
572
|
+
def usb(udid, manager, url, mjpeg, is_primary, saved_name):
|
|
573
|
+
device = by_udid.get(udid)
|
|
574
|
+
over_wifi = False
|
|
575
|
+
if device is None and wifi is not None and udid.upper() in wifi and probe and wda_answers(url):
|
|
576
|
+
# Unplugged, but the Mac app's Wi-Fi relay answers on its port: the phone is there over Wi-Fi.
|
|
577
|
+
device, over_wifi = {"udid": udid, "trusted": True}, True
|
|
578
|
+
state, reason = usb_state(device, manager, (lambda: wda_answers(url)) if probe else (lambda: True),
|
|
579
|
+
(lambda: lease_free(url)) if probe else (lambda: True))
|
|
580
|
+
if wifi is not None and udid.upper() in wifi and state == "disconnected":
|
|
581
|
+
reason = REASONS["wifi_unreachable"]
|
|
582
|
+
model = (device or {}).get("model")
|
|
583
|
+
item = record(id=udid, kind="usb", name=(device or {}).get("name") or saved_name, udid=udid, model=model,
|
|
584
|
+
model_name=MODEL_NAMES.get(model), ios=(device or {}).get("ios"),
|
|
585
|
+
enclosure=(device or {}).get("enclosure"), state=state, reason=reason,
|
|
586
|
+
wda_url_value=url, mjpeg_url=mjpeg, primary=is_primary,
|
|
587
|
+
set_up=manager is not None and manager.built())
|
|
588
|
+
if wifi is not None:
|
|
589
|
+
item["transport"] = "wifi" if over_wifi else "usb" if udid in by_udid else None
|
|
590
|
+
return item
|
|
591
|
+
|
|
592
|
+
chosen = primary.device(connected)
|
|
593
|
+
primary_udid = primary.settings().get("udid") or (chosen or {}).get("udid")
|
|
594
|
+
if primary_udid:
|
|
595
|
+
add(usb(primary_udid, primary, wda_url(WDA_PORT), wda_url(MJPEG_BASE), True,
|
|
596
|
+
primary.settings().get("device_name")))
|
|
597
|
+
for entry in extras:
|
|
598
|
+
if entry["udid"] == primary_udid:
|
|
599
|
+
continue
|
|
600
|
+
add(usb(entry["udid"], manager_for(entry["udid"], data_dir), wda_url(entry["wdaPort"]),
|
|
601
|
+
wda_url(entry["mjpegPort"]), False, entry.get("name")))
|
|
602
|
+
for device in connected:
|
|
603
|
+
if device["udid"] in claimed or device["udid"] == primary_udid:
|
|
604
|
+
continue
|
|
605
|
+
add(record(id=device["udid"], kind="usb", name=device.get("name"), udid=device["udid"],
|
|
606
|
+
model=device.get("model"), model_name=MODEL_NAMES.get(device.get("model")), ios=device.get("ios"),
|
|
607
|
+
enclosure=device.get("enclosure"), state="needs_setup", reason=REASONS["untrusted"]
|
|
608
|
+
if not device.get("trusted") else REASONS["not_built"],
|
|
609
|
+
wda_url_value=None, set_up=False))
|
|
610
|
+
if sims:
|
|
611
|
+
for item in simulator_records(probe=probe):
|
|
612
|
+
add(item)
|
|
613
|
+
for spec in static_specs():
|
|
614
|
+
add(static_record(spec.wda_url, id=spec.id, name=spec.label or None, mjpeg_url=spec.mjpeg_url or None,
|
|
615
|
+
**({} if probe else {"ready": True, "free": True})))
|
|
616
|
+
for url in extra_urls:
|
|
617
|
+
if url and url.rstrip("/") not in seen_urls:
|
|
618
|
+
add(static_record(url, **({} if probe else {"ready": True, "free": True})))
|
|
619
|
+
return records
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def wifi_phones(data_dir):
|
|
623
|
+
from .wireless import store, transport_on
|
|
624
|
+
if not transport_on():
|
|
625
|
+
return None
|
|
626
|
+
try:
|
|
627
|
+
return store.enabled_udids(data_dir)
|
|
628
|
+
except Exception:
|
|
629
|
+
return set()
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def manager_for(udid, data_dir=None):
|
|
633
|
+
"""The DeviceManager of a USB phone set up besides the primary one (devices.json), or None."""
|
|
634
|
+
from .device_manager import DeviceManager
|
|
635
|
+
from .paths import user_data_dir
|
|
636
|
+
data_dir = Path(data_dir) if data_dir else user_data_dir()
|
|
637
|
+
entry = next((item for item in DeviceStore(data_dir).phones() if item["udid"] == udid), None)
|
|
638
|
+
if entry is None:
|
|
639
|
+
return None
|
|
640
|
+
return DeviceManager(data_dir, wda_url(entry["wdaPort"]), udid=udid, root=phone_folder(data_dir, udid),
|
|
641
|
+
video_port=entry["mjpegPort"])
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def resolve(records, query):
|
|
645
|
+
text = str(query or "").strip()
|
|
646
|
+
if not text:
|
|
647
|
+
raise DeviceNotFound("Name a device: its id, UDID or name.")
|
|
648
|
+
folded = text.casefold()
|
|
649
|
+
for item in records:
|
|
650
|
+
if folded in {str(item.get("id") or "").casefold(), str(item.get("udid") or "").casefold()}:
|
|
651
|
+
return item
|
|
652
|
+
named = [item for item in records if str(item.get("name") or "").casefold() == folded]
|
|
653
|
+
if len(named) == 1:
|
|
654
|
+
return named[0]
|
|
655
|
+
if named:
|
|
656
|
+
raise AmbiguousDevice(f"{len(named)} devices are named “{text}”. Name one by its id: "
|
|
657
|
+
+ ", ".join(item["id"] for item in named) + ".")
|
|
658
|
+
known = ", ".join(f"{item['name']} ({item['id']})" for item in records[:8])
|
|
659
|
+
raise DeviceNotFound(f"No device is named or has the id “{text}”." +
|
|
660
|
+
(f" Devices: {known}." if known else " Mobster sees no device: plug in an iPhone or "
|
|
661
|
+
"prepare a simulator (`mobster sim prepare`)."))
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def describe(item):
|
|
665
|
+
"""One line for a terminal: name, kind, state and reason."""
|
|
666
|
+
kind = {"usb": "iPhone (USB)", "simulator": "Simulator", "wda": "WebDriverAgent",
|
|
667
|
+
"android": "Android emulator" if item.get("emulator") else "Android phone"}.get(item["kind"], item["kind"])
|
|
668
|
+
if item["kind"] == "usb" and item.get("transport") == "wifi":
|
|
669
|
+
kind = "iPhone (Wi-Fi)"
|
|
670
|
+
details = ", ".join(part for part in (item.get("modelName") or item.get("model"),
|
|
671
|
+
f"iOS {item['ios']}" if item.get("ios") else None) if part)
|
|
672
|
+
state = item["state"].replace("_", " ")
|
|
673
|
+
line = f"{item['name']} [{kind}{', ' + details if details else ''}] {state}"
|
|
674
|
+
if item.get("reason"):
|
|
675
|
+
line += f": {item['reason']}"
|
|
676
|
+
return line
|