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/fleet.py
ADDED
|
@@ -0,0 +1,841 @@
|
|
|
1
|
+
"""The server's devices (SPEC §3): one ``Phone`` per device, and the ``Fleet`` that lists and resolves them."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import json
|
|
5
|
+
import shutil
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from . import devices as registry
|
|
11
|
+
from . import ownership
|
|
12
|
+
from .devices import REASONS
|
|
13
|
+
from .fleet_phone import Phone, PrimaryPhone
|
|
14
|
+
|
|
15
|
+
# The thread class as imported: tests that park the runtime's workers (patching threading.Thread) don't count the
|
|
16
|
+
# background refresh of the simulator list as one.
|
|
17
|
+
_Thread = threading.Thread
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Fleet:
|
|
21
|
+
"""Every device this server can run tasks on, and which one a request means."""
|
|
22
|
+
|
|
23
|
+
USB_TTL = 2.0
|
|
24
|
+
SIM_TTL = 5.0
|
|
25
|
+
|
|
26
|
+
def __init__(self, runtime):
|
|
27
|
+
self.runtime = runtime
|
|
28
|
+
self.config = runtime.config
|
|
29
|
+
self.lock = threading.RLock()
|
|
30
|
+
self.primary = PrimaryPhone(self)
|
|
31
|
+
self.phones = {} # id -> Phone: USB phones set up here, simulators, WDA addresses
|
|
32
|
+
self.cache = {}
|
|
33
|
+
self.sims_lock = threading.Lock()
|
|
34
|
+
self.closed = False
|
|
35
|
+
manager = getattr(runtime, "manager", None)
|
|
36
|
+
self.data_dir = None
|
|
37
|
+
if manager is not None:
|
|
38
|
+
from .paths import user_data_dir
|
|
39
|
+
self.data_dir = Path(getattr(self.config, "data_dir", None) or user_data_dir())
|
|
40
|
+
self.store = registry.DeviceStore(self.data_dir) if self.data_dir is not None else None
|
|
41
|
+
from .fleet_health import FleetHealth
|
|
42
|
+
self.health = FleetHealth(self)
|
|
43
|
+
# A phone's own settings (a nickname, the default, Disconnect): devices.json in the data folder, else beside the
|
|
44
|
+
# journal (a server without USB phones), else in memory only.
|
|
45
|
+
state_db = getattr(self.config, "state_db", None)
|
|
46
|
+
self.prefs_store = self.store if self.store is not None else registry.DeviceStore(
|
|
47
|
+
Path(state_db).parent if isinstance(state_db, str) and state_db else None)
|
|
48
|
+
self.prefs_cache = None
|
|
49
|
+
stored = self.store.read() if self.store is not None else {"phones": [], "lastUsed": None}
|
|
50
|
+
self.last_used = stored["lastUsed"]
|
|
51
|
+
preferred = getattr(self.config, "device", None) # serve --device
|
|
52
|
+
self.preferred = preferred if isinstance(preferred, str) and preferred else None
|
|
53
|
+
if manager is not None:
|
|
54
|
+
manager.claimed = lambda: {phone.udid for phone in list(self.phones.values())
|
|
55
|
+
if phone.kind == "usb" and phone.manager is not None}
|
|
56
|
+
for entry in stored["phones"]:
|
|
57
|
+
if entry["udid"] == self._chosen():
|
|
58
|
+
continue
|
|
59
|
+
try:
|
|
60
|
+
self._usb_phone(entry)
|
|
61
|
+
except Exception as error: # one phone's folder never stops the others, or the server
|
|
62
|
+
import sys
|
|
63
|
+
print(f"mobster: an iPhone set up earlier couldn't be loaded ({type(error).__name__})",
|
|
64
|
+
file=sys.stderr, flush=True)
|
|
65
|
+
if getattr(runtime, "pool", None) is None:
|
|
66
|
+
for spec in registry.static_specs(self.config):
|
|
67
|
+
if spec.wda_url.rstrip("/") != (self.primary.wda_url or ""):
|
|
68
|
+
self.phones.setdefault(spec.id, Phone(self, id=spec.id, kind="wda", wda_url=spec.wda_url,
|
|
69
|
+
mjpeg_url=spec.mjpeg_url or None,
|
|
70
|
+
name=spec.label or None))
|
|
71
|
+
|
|
72
|
+
# -- USB phones set up here -------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
def _usb_phone(self, entry):
|
|
75
|
+
from .device_info import DeviceInfo
|
|
76
|
+
from .device_manager import DeviceManager
|
|
77
|
+
from .installed_apps import InstalledApps, InventoryCache
|
|
78
|
+
from .setup_service import SetupService
|
|
79
|
+
udid = entry["udid"]
|
|
80
|
+
url, mjpeg = registry.wda_url(entry["wdaPort"]), registry.wda_url(entry["mjpegPort"])
|
|
81
|
+
folder = registry.phone_folder(self.data_dir, udid)
|
|
82
|
+
folder.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
83
|
+
manager = DeviceManager(self.data_dir, url, udid=udid, root=folder, video_port=entry["mjpegPort"])
|
|
84
|
+
primary = self.runtime.manager
|
|
85
|
+
manager.cache = primary.cache # one Xcode, team and USB reading for every phone
|
|
86
|
+
phone = Phone(self, id=udid, kind="usb", wda_url=url, mjpeg_url=mjpeg, udid=udid, name=entry.get("name"),
|
|
87
|
+
manager=manager)
|
|
88
|
+
env_file = self.runtime.setup.env_file if self.runtime.setup is not None else None
|
|
89
|
+
phone.setup = SetupService(self.runtime, manager, env_file, video=lambda: phone.video,
|
|
90
|
+
busy=lambda: self.busy(phone) is not None,
|
|
91
|
+
stop=lambda message: self.runtime.stop_phone_run(phone, message),
|
|
92
|
+
idle=lambda: self.runtime.release_idle(phone))
|
|
93
|
+
phone.device_info = DeviceInfo(manager=manager)
|
|
94
|
+
if not getattr(self.config, "socket", None):
|
|
95
|
+
phone.installed_apps = InstalledApps(manager.device, cache=InventoryCache(self.data_dir / "installed-apps"),
|
|
96
|
+
over_wifi=manager.wifi_udid)
|
|
97
|
+
self.phones[udid] = phone
|
|
98
|
+
threading.Thread(target=manager.watch, args=(manager.watching,), name=f"mobster-device-watch-{udid[-6:]}",
|
|
99
|
+
daemon=True).start()
|
|
100
|
+
return phone
|
|
101
|
+
|
|
102
|
+
def adopt(self, udid):
|
|
103
|
+
manager = self.runtime.manager
|
|
104
|
+
if manager is None:
|
|
105
|
+
raise LookupError("This Mobster doesn't manage USB iPhones. Start it with --manage-device.")
|
|
106
|
+
with self.lock:
|
|
107
|
+
if udid in self.phones and self.phones[udid].manager is not None:
|
|
108
|
+
return self.phones[udid]
|
|
109
|
+
chosen = self._chosen()
|
|
110
|
+
if not chosen or chosen == udid:
|
|
111
|
+
manager.choose(udid)
|
|
112
|
+
return self.primary
|
|
113
|
+
listing = {item["udid"]: item for item in self._usb_listing()}
|
|
114
|
+
if udid not in listing:
|
|
115
|
+
raise LookupError("That iPhone is not connected")
|
|
116
|
+
entry = self.store.add(udid, listing[udid].get("name"))
|
|
117
|
+
return self._usb_phone(entry)
|
|
118
|
+
|
|
119
|
+
def forget(self, query):
|
|
120
|
+
phone = self.find(query)
|
|
121
|
+
if phone.kind != "usb" or phone.manager is None:
|
|
122
|
+
if phone.kind == "simulator":
|
|
123
|
+
raise LookupError("Mobster's simulators are managed with `mobster sim`. Remove it there.")
|
|
124
|
+
if phone.kind == "wda":
|
|
125
|
+
raise LookupError("This device comes from MOBSTER_WDA_DEVICES. Remove it there.")
|
|
126
|
+
raise LookupError("This iPhone isn't set up in Mobster, so there's nothing to forget.")
|
|
127
|
+
# Checked and dropped under the runtime's lock, which admits tasks: none can start on it in between.
|
|
128
|
+
with self.runtime.lock:
|
|
129
|
+
if self.busy(phone) is not None:
|
|
130
|
+
raise LookupError("A task is running on it. Stop it first.")
|
|
131
|
+
if not phone.primary:
|
|
132
|
+
with self.lock:
|
|
133
|
+
self.phones.pop(phone.id, None)
|
|
134
|
+
udid, forgotten = phone.udid, phone.id
|
|
135
|
+
if phone.primary:
|
|
136
|
+
self._forget_primary(phone)
|
|
137
|
+
else:
|
|
138
|
+
phone.close()
|
|
139
|
+
self.store.remove(udid)
|
|
140
|
+
with contextlib.suppress(OSError):
|
|
141
|
+
shutil.rmtree(registry.phone_folder(self.data_dir, udid))
|
|
142
|
+
self._drop_prefs(forgotten, udid)
|
|
143
|
+
self.release_profiles(forgotten)
|
|
144
|
+
with contextlib.suppress(OSError):
|
|
145
|
+
(self.data_dir / "installed-apps" / f"{udid}.json").unlink()
|
|
146
|
+
self.cache.pop("usb", None) # the USB listing only: every phone keeps its lastUsedAt
|
|
147
|
+
return forgotten
|
|
148
|
+
|
|
149
|
+
def _forget_primary(self, phone):
|
|
150
|
+
manager = phone.manager
|
|
151
|
+
manager.stop_runner(remember=True)
|
|
152
|
+
settings = {key: value for key, value in manager.settings().items()
|
|
153
|
+
if key not in ("udid", "device_name", "built_for", "built_patches", "autostart", "expires_at")}
|
|
154
|
+
with contextlib.suppress(OSError):
|
|
155
|
+
registry_dir = manager.settings_path.parent
|
|
156
|
+
registry_dir.mkdir(parents=True, exist_ok=True)
|
|
157
|
+
tmp = manager.settings_path.with_suffix(".tmp")
|
|
158
|
+
tmp.write_text(json.dumps(settings, indent=1))
|
|
159
|
+
tmp.chmod(0o600)
|
|
160
|
+
tmp.replace(manager.settings_path)
|
|
161
|
+
with contextlib.suppress(OSError):
|
|
162
|
+
shutil.rmtree(manager.derived)
|
|
163
|
+
phone.id = phone.udid = None
|
|
164
|
+
phone.name = None
|
|
165
|
+
|
|
166
|
+
def release_profiles(self, device_id):
|
|
167
|
+
# to the default phone, so a task as one of them starts instead of being refused for a phone that is gone
|
|
168
|
+
from .accounts.store import store_for
|
|
169
|
+
with contextlib.suppress(OSError):
|
|
170
|
+
store_for(self.runtime).forget_device(device_id)
|
|
171
|
+
|
|
172
|
+
def _drop_prefs(self, *ids):
|
|
173
|
+
for device_id in {item for item in ids if item}:
|
|
174
|
+
with contextlib.suppress(OSError, ValueError):
|
|
175
|
+
self.prefs_store.remove(device_id)
|
|
176
|
+
if self.last_used == device_id:
|
|
177
|
+
self.last_used = None
|
|
178
|
+
self.prefs_cache = None
|
|
179
|
+
|
|
180
|
+
# -- a phone's own settings (Settings › Phones) ------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
def stored(self):
|
|
183
|
+
"""{prefs, defaultDevice, disconnected, chosen}: read from devices.json at most once per change of the file."""
|
|
184
|
+
store = self.prefs_store
|
|
185
|
+
stamp = None
|
|
186
|
+
if store.path is not None: # in memory there is no file to stamp, and reading is free
|
|
187
|
+
try:
|
|
188
|
+
stamp = store.path.stat().st_mtime_ns
|
|
189
|
+
except OSError:
|
|
190
|
+
pass
|
|
191
|
+
cached = self.prefs_cache
|
|
192
|
+
if cached is not None and cached[0] == stamp:
|
|
193
|
+
return cached[1]
|
|
194
|
+
data = store.read()
|
|
195
|
+
value = {"prefs": data["prefs"], "defaultDevice": data["defaultDevice"], "disconnected": data["disconnected"],
|
|
196
|
+
"chosen": data["chosen"]}
|
|
197
|
+
if store.path is not None:
|
|
198
|
+
self.prefs_cache = (stamp, value)
|
|
199
|
+
return value
|
|
200
|
+
|
|
201
|
+
def prefs(self, phone):
|
|
202
|
+
return dict(self.stored()["prefs"].get(phone.id) or {}) if phone is not None and phone.id else {}
|
|
203
|
+
|
|
204
|
+
def is_disconnected(self, phone):
|
|
205
|
+
return bool(phone is not None and phone.id and phone.id in self.stored()["disconnected"])
|
|
206
|
+
|
|
207
|
+
def set_prefs(self, phone, **changes):
|
|
208
|
+
if not phone.id:
|
|
209
|
+
raise LookupError("This phone has no identity yet. Plug it in and unlock it.")
|
|
210
|
+
value = self.prefs_store.set_prefs(phone.id, **changes)
|
|
211
|
+
self.prefs_cache = None
|
|
212
|
+
if "nickname" in changes:
|
|
213
|
+
phone.name = value.get("nickname") or phone.name
|
|
214
|
+
return value
|
|
215
|
+
|
|
216
|
+
def set_default(self, phone):
|
|
217
|
+
"""Make ``phone`` the one a new task runs on unless another is picked (None: no default of your own)."""
|
|
218
|
+
device_id = phone.id if phone is not None else None
|
|
219
|
+
if phone is not None and not device_id:
|
|
220
|
+
raise LookupError("This phone has no identity yet. Plug it in and unlock it.")
|
|
221
|
+
self.prefs_store.set_default(device_id)
|
|
222
|
+
self.prefs_cache = None
|
|
223
|
+
return device_id
|
|
224
|
+
|
|
225
|
+
# -- the composer's phone (the Mac app) -----------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
def choose(self, device_ids):
|
|
228
|
+
clean = registry.clean_chosen(device_ids) if isinstance(device_ids, (list, tuple)) else None
|
|
229
|
+
if clean is None or len(clean) != len(device_ids):
|
|
230
|
+
raise ValueError(f"devices is a list of at most {registry.MAX_CHOSEN} different device ids")
|
|
231
|
+
self.prefs_store.set_chosen(clean)
|
|
232
|
+
self.prefs_cache = None
|
|
233
|
+
return clean
|
|
234
|
+
|
|
235
|
+
def chosen(self, records=None):
|
|
236
|
+
"""Keep a connected choice, except one needing setup while another is set up; never save the fallback."""
|
|
237
|
+
records = self.public() if records is None else records
|
|
238
|
+
listed = {item["id"]: item for item in records if item.get("id")}
|
|
239
|
+
kept = [device_id for device_id in self.stored()["chosen"]
|
|
240
|
+
if device_id in listed and listed[device_id]["state"] != "disconnected"]
|
|
241
|
+
if kept and not (all(listed[device_id]["state"] == "needs_setup" for device_id in kept)
|
|
242
|
+
and any(item["state"] not in ("needs_setup", "disconnected") for item in records)):
|
|
243
|
+
return kept
|
|
244
|
+
if not records: # the fallback below is never saved, so the chosen phone comes back when plugged in
|
|
245
|
+
return []
|
|
246
|
+
rank = {"ready": 0, "busy": 1, "connected": 2, "needs_setup": 3, "disconnected": 4}
|
|
247
|
+
best = min(enumerate(records), key=lambda pair: (rank.get(pair[1]["state"], 4), not pair[1].get("default"),
|
|
248
|
+
not pair[1].get("primary"), pair[0]))[1]
|
|
249
|
+
return [best["id"]] if best.get("id") else []
|
|
250
|
+
|
|
251
|
+
def disconnect(self, phone):
|
|
252
|
+
if not phone.id:
|
|
253
|
+
raise LookupError("This phone has no identity yet.")
|
|
254
|
+
if phone.kind == "usb" and phone.manager is None:
|
|
255
|
+
raise LookupError("This iPhone isn't set up yet, so it isn't in use.")
|
|
256
|
+
with self.runtime.lock:
|
|
257
|
+
if self.busy(phone) is not None:
|
|
258
|
+
raise LookupError("A task is running on it. Stop it first.")
|
|
259
|
+
self._set_disconnected(phone.id, True)
|
|
260
|
+
if phone.manager is not None:
|
|
261
|
+
with contextlib.suppress(Exception):
|
|
262
|
+
phone.manager.stop_runner(remember=True)
|
|
263
|
+
if not phone.primary:
|
|
264
|
+
for name in ("_video", "_control"):
|
|
265
|
+
item = getattr(phone, name, None)
|
|
266
|
+
if item is not None:
|
|
267
|
+
with contextlib.suppress(Exception):
|
|
268
|
+
item.close()
|
|
269
|
+
setattr(phone, name, None)
|
|
270
|
+
phone.health_at = 0.0
|
|
271
|
+
return phone.id
|
|
272
|
+
|
|
273
|
+
def connect(self, phone):
|
|
274
|
+
if not phone.id:
|
|
275
|
+
raise LookupError("This phone has no identity yet.")
|
|
276
|
+
self._set_disconnected(phone.id, False)
|
|
277
|
+
note = None
|
|
278
|
+
manager = phone.manager
|
|
279
|
+
if manager is not None:
|
|
280
|
+
with contextlib.suppress(OSError):
|
|
281
|
+
manager.save_settings(autostart=True) # the watcher starts it when the phone is back
|
|
282
|
+
try:
|
|
283
|
+
manager.start_runner()
|
|
284
|
+
except LookupError as error:
|
|
285
|
+
note = str(error)
|
|
286
|
+
except Exception: # noqa: BLE001 -- the watcher tries again
|
|
287
|
+
note = "Mobster will start its helper on it in a moment."
|
|
288
|
+
phone.health_at = 0.0
|
|
289
|
+
return note
|
|
290
|
+
|
|
291
|
+
def _set_disconnected(self, device_id, value):
|
|
292
|
+
self.prefs_store.set_disconnected(device_id, value)
|
|
293
|
+
self.prefs_cache = None
|
|
294
|
+
|
|
295
|
+
# -- discovery --------------------------------------------------------------------------------------
|
|
296
|
+
|
|
297
|
+
def _usb_listing(self):
|
|
298
|
+
manager = getattr(self.runtime, "manager", None)
|
|
299
|
+
if manager is None:
|
|
300
|
+
return []
|
|
301
|
+
cached = self.cache.get("usb")
|
|
302
|
+
if cached and time.monotonic() - cached[0] < self.USB_TTL:
|
|
303
|
+
return cached[1]
|
|
304
|
+
try:
|
|
305
|
+
listing = [item for item in manager.devices() if isinstance(item, dict)
|
|
306
|
+
and isinstance(item.get("udid"), str)]
|
|
307
|
+
except Exception:
|
|
308
|
+
listing = []
|
|
309
|
+
self.cache["usb"] = (time.monotonic(), listing)
|
|
310
|
+
return listing
|
|
311
|
+
|
|
312
|
+
def _sims_wanted(self):
|
|
313
|
+
url = self.primary.wda_url or ""
|
|
314
|
+
# No WDA target (a scripted phone, the terminal UI's demo): no simulators beside it.
|
|
315
|
+
return getattr(self.runtime, "manager", None) is not None or url.startswith(("http://", "https://"))
|
|
316
|
+
|
|
317
|
+
def _read_sims(self):
|
|
318
|
+
try:
|
|
319
|
+
rows = registry.simulator_rows()
|
|
320
|
+
except Exception:
|
|
321
|
+
rows = []
|
|
322
|
+
self.cache["sims"] = (time.monotonic(), rows)
|
|
323
|
+
return rows
|
|
324
|
+
|
|
325
|
+
def _sim_rows(self):
|
|
326
|
+
# simctl takes up to a second: a stale list is served while a fresh one is read in the background
|
|
327
|
+
if not self._sims_wanted():
|
|
328
|
+
return []
|
|
329
|
+
cached = self.cache.get("sims")
|
|
330
|
+
if cached is None:
|
|
331
|
+
with self.sims_lock:
|
|
332
|
+
cached = self.cache.get("sims")
|
|
333
|
+
return cached[1] if cached is not None else self._read_sims()
|
|
334
|
+
if time.monotonic() - cached[0] >= self.SIM_TTL and self.sims_lock.acquire(blocking=False):
|
|
335
|
+
def refresh():
|
|
336
|
+
try:
|
|
337
|
+
self._read_sims()
|
|
338
|
+
finally:
|
|
339
|
+
self.sims_lock.release()
|
|
340
|
+
_Thread(target=refresh, name="mobster-fleet-sims", daemon=True).start()
|
|
341
|
+
return cached[1]
|
|
342
|
+
|
|
343
|
+
def _sim_phone(self, row):
|
|
344
|
+
with self.lock:
|
|
345
|
+
phone = self.phones.get(row["udid"])
|
|
346
|
+
if phone is None or phone.kind != "simulator" or phone.wda_url != row["wda_url"].rstrip("/"):
|
|
347
|
+
phone = Phone(self, id=row["udid"], kind="simulator", wda_url=row["wda_url"],
|
|
348
|
+
mjpeg_url=row.get("mjpeg_url"), udid=row["udid"], name=row.get("name"),
|
|
349
|
+
model=row.get("device_type"),
|
|
350
|
+
ios=str(row.get("runtime") or "").removeprefix("iOS ").strip() or None)
|
|
351
|
+
self.phones[row["udid"]] = phone
|
|
352
|
+
return phone
|
|
353
|
+
|
|
354
|
+
def _chosen(self):
|
|
355
|
+
"""The UDID chosen in Setup for the primary phone, or None."""
|
|
356
|
+
manager = getattr(self.runtime, "manager", None)
|
|
357
|
+
try:
|
|
358
|
+
udid = manager.settings().get("udid") if manager is not None else None
|
|
359
|
+
except Exception:
|
|
360
|
+
return None
|
|
361
|
+
return udid if isinstance(udid, str) and udid else None
|
|
362
|
+
|
|
363
|
+
def _primary_identity(self):
|
|
364
|
+
manager = getattr(self.runtime, "manager", None)
|
|
365
|
+
url = self.primary.wda_url
|
|
366
|
+
if manager is not None:
|
|
367
|
+
listing = self._usb_listing()
|
|
368
|
+
udid = self._chosen()
|
|
369
|
+
if udid is None:
|
|
370
|
+
try:
|
|
371
|
+
found = manager.device(listing)
|
|
372
|
+
except Exception:
|
|
373
|
+
found = None
|
|
374
|
+
udid = found.get("udid") if isinstance(found, dict) else None
|
|
375
|
+
if not isinstance(udid, str) or not udid:
|
|
376
|
+
return None
|
|
377
|
+
device = next((item for item in listing if item["udid"] == udid), None)
|
|
378
|
+
saved = manager.settings().get("device_name")
|
|
379
|
+
return {"id": udid, "kind": "usb", "udid": udid, "device": device,
|
|
380
|
+
"name": (device or {}).get("name") or (saved if isinstance(saved, str) else None)}
|
|
381
|
+
if not url:
|
|
382
|
+
return None
|
|
383
|
+
for row in self._sim_rows():
|
|
384
|
+
if row["wda_url"].rstrip("/") == url:
|
|
385
|
+
return {"id": row["udid"], "kind": "simulator", "udid": row["udid"], "row": row,
|
|
386
|
+
"name": row.get("name")}
|
|
387
|
+
return {"id": registry.device_id_for_url(url), "kind": "wda", "udid": None, "name": None}
|
|
388
|
+
|
|
389
|
+
def busy(self, phone):
|
|
390
|
+
run_on = getattr(self.runtime, "run_on", None)
|
|
391
|
+
if callable(run_on):
|
|
392
|
+
return run_on(phone)
|
|
393
|
+
return next((run_id for run_id, key in list(self.runtime.active_runs.items()) if key == phone.key), None)
|
|
394
|
+
|
|
395
|
+
def in_use(self, phone):
|
|
396
|
+
if self.busy(phone) is not None:
|
|
397
|
+
return True
|
|
398
|
+
url = phone.wda_url
|
|
399
|
+
return bool(url) and not self._lease_free(url, phone.udid)
|
|
400
|
+
|
|
401
|
+
def _lease_free(self, url, udid=None):
|
|
402
|
+
# Under the runtime's lock: Runtime.create takes the same lease under it, and two flocks in one process
|
|
403
|
+
# on one file would make a task refused while this probe holds it for a moment. Never called with that
|
|
404
|
+
# lock held (create resolves its device first).
|
|
405
|
+
with self.runtime.lock:
|
|
406
|
+
return registry.lease_free(url, udid)
|
|
407
|
+
|
|
408
|
+
def inventory(self):
|
|
409
|
+
found = []
|
|
410
|
+
identity = self._primary_identity()
|
|
411
|
+
primary_url = self.primary.wda_url
|
|
412
|
+
if identity is not None:
|
|
413
|
+
phone = self.primary
|
|
414
|
+
phone.id, phone.kind, phone.udid = identity["id"], identity["kind"], identity["udid"]
|
|
415
|
+
phone.name = identity.get("name") or phone.name
|
|
416
|
+
found.append((phone, identity))
|
|
417
|
+
with self.lock:
|
|
418
|
+
usb_phones = [phone for phone in self.phones.values() if phone.kind == "usb" and phone.manager is not None]
|
|
419
|
+
claimed = {phone.udid for phone in usb_phones}
|
|
420
|
+
if identity is not None and identity["kind"] == "usb":
|
|
421
|
+
claimed.add(identity["udid"])
|
|
422
|
+
listing = {item["udid"]: item for item in self._usb_listing()}
|
|
423
|
+
for phone in usb_phones:
|
|
424
|
+
device = listing.get(phone.udid)
|
|
425
|
+
if device is not None:
|
|
426
|
+
phone.name = device.get("name") or phone.name
|
|
427
|
+
phone.model, phone.ios = device.get("model"), device.get("ios")
|
|
428
|
+
found.append((phone, {"device": device}))
|
|
429
|
+
for udid, device in listing.items():
|
|
430
|
+
if udid in claimed:
|
|
431
|
+
continue
|
|
432
|
+
with self.lock:
|
|
433
|
+
loose = self.phones.get(udid)
|
|
434
|
+
if loose is None or loose.manager is not None:
|
|
435
|
+
loose = self.phones[udid] = Phone(self, id=udid, kind="usb", udid=udid)
|
|
436
|
+
loose.name, loose.model, loose.ios = device.get("name"), device.get("model"), device.get("ios")
|
|
437
|
+
found.append((loose, {"device": device, "loose": True}))
|
|
438
|
+
seen = {primary_url} if primary_url else set()
|
|
439
|
+
seen |= {phone.wda_url for phone in usb_phones}
|
|
440
|
+
for row in self._sim_rows():
|
|
441
|
+
if row["wda_url"].rstrip("/") in seen:
|
|
442
|
+
continue
|
|
443
|
+
phone = self._sim_phone(row)
|
|
444
|
+
found.append((phone, {"row": row}))
|
|
445
|
+
seen.add(phone.wda_url)
|
|
446
|
+
with self.lock:
|
|
447
|
+
static = [phone for phone in self.phones.values() if phone.kind == "wda"]
|
|
448
|
+
for phone in static:
|
|
449
|
+
if phone.wda_url not in seen:
|
|
450
|
+
found.append((phone, {}))
|
|
451
|
+
seen.add(phone.wda_url)
|
|
452
|
+
# A nickname given in Settings › Phones is the phone's name everywhere (the device list, runs, conversations).
|
|
453
|
+
nicknames = self.stored()["prefs"]
|
|
454
|
+
for phone, _ in found:
|
|
455
|
+
nickname = (nicknames.get(phone.id) or {}).get("nickname") if phone.id else None
|
|
456
|
+
if nickname:
|
|
457
|
+
phone.name = nickname
|
|
458
|
+
# Devices that went away (a simulator deleted, a phone unplugged before it was set up) are dropped.
|
|
459
|
+
live = {id(phone) for phone, _ in found}
|
|
460
|
+
with self.lock:
|
|
461
|
+
for key in [key for key, phone in self.phones.items() if id(phone) not in live and phone.manager is None
|
|
462
|
+
and phone.kind != "wda" and self.busy(phone) is None]:
|
|
463
|
+
self.phones.pop(key, None)
|
|
464
|
+
return found
|
|
465
|
+
|
|
466
|
+
def usb_managers(self):
|
|
467
|
+
out = []
|
|
468
|
+
manager = getattr(self.runtime, "manager", None)
|
|
469
|
+
udid = self._chosen() if manager is not None else None
|
|
470
|
+
if udid:
|
|
471
|
+
out.append((self.primary, udid, manager))
|
|
472
|
+
with self.lock:
|
|
473
|
+
others = [phone for phone in self.phones.values() if phone.kind == "usb" and phone.manager is not None]
|
|
474
|
+
out += [(phone, phone.udid, phone.manager) for phone in others if phone.udid and phone.udid != udid]
|
|
475
|
+
return out
|
|
476
|
+
|
|
477
|
+
def _wifi(self):
|
|
478
|
+
return registry.wifi_phones(self.data_dir) if self.data_dir is not None else None
|
|
479
|
+
|
|
480
|
+
def entries(self, found=None):
|
|
481
|
+
from .device_info import MODEL_NAMES
|
|
482
|
+
# one phone's probe (``entry``) isn't every phone: the alerts' baseline needs every one
|
|
483
|
+
complete = found is None
|
|
484
|
+
found = self.inventory() if found is None else found
|
|
485
|
+
default = self.default(found)
|
|
486
|
+
chosen_default = self.stored()["defaultDevice"] # Settings › Phones › Set as default (not the phone used last)
|
|
487
|
+
wifi = self._wifi()
|
|
488
|
+
rows = []
|
|
489
|
+
for phone, context in found:
|
|
490
|
+
if context.get("loose"):
|
|
491
|
+
device = context["device"]
|
|
492
|
+
item = registry.record(
|
|
493
|
+
id=phone.id, kind="usb", name=phone.name, udid=phone.udid, model=phone.model,
|
|
494
|
+
model_name=MODEL_NAMES.get(phone.model), ios=phone.ios, state="needs_setup",
|
|
495
|
+
reason=REASONS["not_built"] if device.get("trusted") else REASONS["untrusted"],
|
|
496
|
+
wda_url_value=None, set_up=False)
|
|
497
|
+
else:
|
|
498
|
+
item = self._record(phone, context, wifi)
|
|
499
|
+
run_id = self.busy(phone)
|
|
500
|
+
run = self.runtime.runs.get(run_id) if run_id else None
|
|
501
|
+
approval = None
|
|
502
|
+
if run is not None:
|
|
503
|
+
with run.condition:
|
|
504
|
+
approval = dict(run.approval) if run.approval else None
|
|
505
|
+
item = {key: value for key, value in item.items() if key not in ("wdaUrl", "mjpegUrl")}
|
|
506
|
+
prefs = self.prefs(phone)
|
|
507
|
+
disconnected = self.is_disconnected(phone)
|
|
508
|
+
if disconnected and run_id is None and item["state"] != "needs_setup":
|
|
509
|
+
item.update(state="disconnected", reason=REASONS["paused"])
|
|
510
|
+
item.update(default=phone is default, activeRunId=run_id, approval=approval,
|
|
511
|
+
lastUsedAt=self.last_used_at(phone), nickname=prefs.get("nickname"),
|
|
512
|
+
disconnected=disconnected, askBeforeActing=prefs.get("askBeforeActing"),
|
|
513
|
+
apps=prefs.get("apps"), defaultSet=bool(phone.id) and phone.id == chosen_default)
|
|
514
|
+
if wifi is not None and item["kind"] == "usb":
|
|
515
|
+
item.update(self._transport_fields(phone, context, wifi))
|
|
516
|
+
# How the screen and the taps reach it: "wda" (Pro), "mirroring" or "cable_bt" (Cable + Bluetooth). A
|
|
517
|
+
# connection that sets its own on the phone wins.
|
|
518
|
+
item["connection"] = getattr(phone, "connection", None) or "wda"
|
|
519
|
+
item.update(self._ownership_fields(phone, item))
|
|
520
|
+
item.update(self._reliability_fields(phone))
|
|
521
|
+
rows.append((phone, item))
|
|
522
|
+
# Last, so the fields other work adds to a record (the owner, the holder) are there to read.
|
|
523
|
+
try:
|
|
524
|
+
self.health.apply(rows, complete=complete)
|
|
525
|
+
except Exception: # noqa: BLE001 -- the device list never fails for health
|
|
526
|
+
pass
|
|
527
|
+
return rows
|
|
528
|
+
|
|
529
|
+
def _ownership_fields(self, phone, item):
|
|
530
|
+
read = getattr(self.runtime, "phone_ownership", None)
|
|
531
|
+
if not callable(read) or item.get("state") == "needs_setup":
|
|
532
|
+
return {}
|
|
533
|
+
try:
|
|
534
|
+
fields = read(phone)
|
|
535
|
+
except Exception: # noqa: BLE001 -- a device list is still a device list without ownership
|
|
536
|
+
return {}
|
|
537
|
+
if fields["holder"] is None and item.get("state") == "busy" and item.get("reason") == REASONS["busy_elsewhere"]:
|
|
538
|
+
fields = {**fields, "holder": ownership.elsewhere_holder()}
|
|
539
|
+
return fields
|
|
540
|
+
|
|
541
|
+
@staticmethod
|
|
542
|
+
def _reliability_fields(phone):
|
|
543
|
+
manager = getattr(phone, "manager", None)
|
|
544
|
+
fields = {}
|
|
545
|
+
if getattr(manager, "presence", None) is not None:
|
|
546
|
+
fields["owner"] = manager.presence.snapshot()
|
|
547
|
+
if getattr(manager, "watchdog", None) is not None:
|
|
548
|
+
fields["watchdog"] = manager.watchdog.snapshot()
|
|
549
|
+
renewal = getattr(manager, "renewal", None)
|
|
550
|
+
if renewal is not None:
|
|
551
|
+
try:
|
|
552
|
+
fields["renewal"] = renewal.public()
|
|
553
|
+
except Exception: # noqa: BLE001 -- a record never fails for a field that is only advice
|
|
554
|
+
pass
|
|
555
|
+
return fields
|
|
556
|
+
|
|
557
|
+
@staticmethod
|
|
558
|
+
def _transport_fields(phone, context, wifi):
|
|
559
|
+
from .wireless import transport
|
|
560
|
+
link = transport.link(phone.udid) if phone.udid else None
|
|
561
|
+
manager = phone.manager
|
|
562
|
+
now = getattr(manager, "transport", None) if manager is not None and manager.runner is not None else None
|
|
563
|
+
if now is None and context.get("device") is not None:
|
|
564
|
+
now = "usb"
|
|
565
|
+
return {"transport": now, "wifi": {"enabled": str(phone.udid or "").upper() in wifi,
|
|
566
|
+
"reachable": bool(link is not None and link.reachable
|
|
567
|
+
and link.transport == "wifi")}}
|
|
568
|
+
|
|
569
|
+
def _record(self, phone, context, wifi=None):
|
|
570
|
+
from .device_info import MODEL_NAMES
|
|
571
|
+
if phone.kind == "simulator":
|
|
572
|
+
return self._sim_record(phone, context["row"])
|
|
573
|
+
if phone.kind == "wda":
|
|
574
|
+
return self._static_record(phone)
|
|
575
|
+
device = context.get("device")
|
|
576
|
+
wifi_on = wifi is not None and str(phone.udid or "").upper() in wifi
|
|
577
|
+
manager = phone.manager
|
|
578
|
+
if device is None and wifi_on and manager is not None and getattr(manager, "transport", None) == "wifi":
|
|
579
|
+
# Unplugged, and its runner runs over Wi-Fi: the phone is here, through the encrypted tunnel.
|
|
580
|
+
device = {"udid": phone.udid, "trusted": True}
|
|
581
|
+
if self.busy(phone) is not None:
|
|
582
|
+
state, reason = "busy", REASONS["busy"]
|
|
583
|
+
else:
|
|
584
|
+
target = phone.target_status() if device is not None and device.get("trusted") else {}
|
|
585
|
+
answered = bool(target.get("ready") or target.get("health"))
|
|
586
|
+
state, reason = registry.usb_state(device, phone.manager, lambda: answered,
|
|
587
|
+
lambda: self._lease_free(phone.wda_url, phone.udid),
|
|
588
|
+
locked=bool(target.get("health")))
|
|
589
|
+
if state == "connected" and target.get("health"):
|
|
590
|
+
# The locked sentence is REASONS["locked"], which fleet_health keys its alert on.
|
|
591
|
+
reason = REASONS["locked"] if phone.manager.locked() else target["health"]
|
|
592
|
+
if state == "disconnected" and wifi_on:
|
|
593
|
+
reason = REASONS["wifi_unreachable"]
|
|
594
|
+
built = phone.manager is not None and phone.manager.built()
|
|
595
|
+
return registry.record(id=phone.id, kind="usb", name=phone.name, udid=phone.udid, model=phone.model,
|
|
596
|
+
model_name=MODEL_NAMES.get(phone.model), ios=phone.ios, state=state, reason=reason,
|
|
597
|
+
wda_url_value=phone.wda_url, mjpeg_url=phone.mjpeg_url, primary=phone.primary,
|
|
598
|
+
set_up=built)
|
|
599
|
+
|
|
600
|
+
def _sim_record(self, phone, row):
|
|
601
|
+
busy_here = self.busy(phone) is not None
|
|
602
|
+
ready = None
|
|
603
|
+
if not busy_here and row.get("state") == "Booted":
|
|
604
|
+
ready = phone.target_status()["ready"]
|
|
605
|
+
item = registry.simulator_record(row, ready=ready, free=None if busy_here else self._lease_free(phone.wda_url)
|
|
606
|
+
if ready else True, busy_here=busy_here)
|
|
607
|
+
if phone.start_state["state"] == "starting" and item["state"] != "ready":
|
|
608
|
+
item.update(state="connected", reason="Starting the simulator and Mobster's helper on it.")
|
|
609
|
+
elif phone.start_state["state"] == "failed" and item["state"] != "ready":
|
|
610
|
+
item["reason"] = phone.start_state["error"] or item["reason"]
|
|
611
|
+
return {**item, "primary": phone.primary}
|
|
612
|
+
|
|
613
|
+
def _static_record(self, phone):
|
|
614
|
+
busy_here = self.busy(phone) is not None
|
|
615
|
+
if busy_here:
|
|
616
|
+
item = registry.record(id=phone.id, kind="wda", name=phone.name or f"WebDriverAgent on port "
|
|
617
|
+
f"{registry.port_of(phone.wda_url)}", state="busy", reason=REASONS["busy"],
|
|
618
|
+
wda_url_value=phone.wda_url)
|
|
619
|
+
else:
|
|
620
|
+
ready = phone.target_status()["ready"]
|
|
621
|
+
item = registry.static_record(phone.wda_url, id=phone.id, name=phone.name, ready=ready,
|
|
622
|
+
free=self._lease_free(phone.wda_url) if ready else True)
|
|
623
|
+
return {**item, "primary": phone.primary}
|
|
624
|
+
|
|
625
|
+
# -- what a request means -----------------------------------------------------------------------------
|
|
626
|
+
|
|
627
|
+
def public(self):
|
|
628
|
+
return [item for _, item in self.entries()]
|
|
629
|
+
|
|
630
|
+
def find(self, query, found=None):
|
|
631
|
+
found = self.inventory() if found is None else found
|
|
632
|
+
rows = [(phone, self._name_row(phone)) for phone, _ in found]
|
|
633
|
+
item = registry.resolve([row for _, row in rows], query)
|
|
634
|
+
return next(phone for phone, row in rows if row is item)
|
|
635
|
+
|
|
636
|
+
def lookup(self, query):
|
|
637
|
+
known = self._known(query)
|
|
638
|
+
return known if known is not None else self.find(query)
|
|
639
|
+
|
|
640
|
+
def _known(self, query):
|
|
641
|
+
folded = str(query or "").strip().casefold()
|
|
642
|
+
if not folded:
|
|
643
|
+
return None
|
|
644
|
+
primary = self.primary
|
|
645
|
+
if primary.id and folded in {primary.id.casefold(), str(primary.udid or "").casefold()}:
|
|
646
|
+
manager = getattr(self.runtime, "manager", None)
|
|
647
|
+
stable = manager is None or (primary.kind == "usb" and self._chosen() == primary.udid)
|
|
648
|
+
return primary if stable else None
|
|
649
|
+
with self.lock:
|
|
650
|
+
phones = list(self.phones.values())
|
|
651
|
+
for phone in phones:
|
|
652
|
+
if phone.kind == "usb" and phone.manager is None: # not set up: Setup may adopt it any moment
|
|
653
|
+
continue
|
|
654
|
+
if phone.id and folded in {phone.id.casefold(), str(phone.udid or "").casefold()}:
|
|
655
|
+
return phone
|
|
656
|
+
return None
|
|
657
|
+
|
|
658
|
+
def entry(self, query):
|
|
659
|
+
"""(phone, public record) for one device, probing only that one."""
|
|
660
|
+
found = self.inventory()
|
|
661
|
+
phone = self.find(query, found)
|
|
662
|
+
return next(row for row in self.entries([(candidate, context) for candidate, context in found
|
|
663
|
+
if candidate is phone]))
|
|
664
|
+
|
|
665
|
+
def default(self, found=None, sticky=True):
|
|
666
|
+
"""Resolve explicit preferences before a usable last-used phone; unattended tasks keep the primary fallback."""
|
|
667
|
+
found = self.inventory() if found is None else found
|
|
668
|
+
phones = [phone for phone, _ in found]
|
|
669
|
+
if self.preferred:
|
|
670
|
+
try:
|
|
671
|
+
return self.find(self.preferred, found)
|
|
672
|
+
except LookupError:
|
|
673
|
+
pass
|
|
674
|
+
chosen = self.stored()["defaultDevice"] # Settings › Phones › Set as default
|
|
675
|
+
if chosen:
|
|
676
|
+
match = next((phone for phone in phones if phone.id == chosen), None)
|
|
677
|
+
if match is not None:
|
|
678
|
+
return match
|
|
679
|
+
if len(phones) == 1:
|
|
680
|
+
return phones[0]
|
|
681
|
+
if not sticky:
|
|
682
|
+
return self.primary
|
|
683
|
+
unready = {id(phone) for phone, context in found if self._needs_setup(phone, context)}
|
|
684
|
+
usable = [phone for phone in phones if id(phone) not in unready]
|
|
685
|
+
if self.last_used:
|
|
686
|
+
match = next((phone for phone in phones if phone.id == self.last_used), None)
|
|
687
|
+
if match is not None and (id(match) not in unready or not usable):
|
|
688
|
+
return match
|
|
689
|
+
if not usable or any(phone is self.primary for phone in usable):
|
|
690
|
+
return self.primary
|
|
691
|
+
return max(usable, key=lambda phone: self.last_used_at(phone) or 0)
|
|
692
|
+
|
|
693
|
+
@staticmethod
|
|
694
|
+
def _needs_setup(phone, context):
|
|
695
|
+
"""Whether identity or build state requires setup, without probing the phone."""
|
|
696
|
+
if context.get("loose"):
|
|
697
|
+
return True
|
|
698
|
+
if phone.kind != "usb":
|
|
699
|
+
return False
|
|
700
|
+
device = context.get("device")
|
|
701
|
+
if device is not None and not device.get("trusted"):
|
|
702
|
+
return True
|
|
703
|
+
manager = phone.manager
|
|
704
|
+
try:
|
|
705
|
+
return manager is None or not manager.built()
|
|
706
|
+
except Exception: # noqa: BLE001 -- a helper folder it can't read is a phone it can't use yet
|
|
707
|
+
return True
|
|
708
|
+
|
|
709
|
+
@staticmethod
|
|
710
|
+
def _name_row(phone):
|
|
711
|
+
return {"id": phone.id or "", "udid": phone.udid, "name": phone.name}
|
|
712
|
+
|
|
713
|
+
def for_run(self, query):
|
|
714
|
+
if query is None:
|
|
715
|
+
return self.default()
|
|
716
|
+
if not isinstance(query, str) or not 1 <= len(query.strip()) <= 128:
|
|
717
|
+
raise ValueError("device must be a device's id, UDID or name")
|
|
718
|
+
return self.find(query) # a phone plugged in but not set up too: admission refuses it as not ready
|
|
719
|
+
|
|
720
|
+
# -- Setup, per device ---------------------------------------------------------------------------------
|
|
721
|
+
|
|
722
|
+
def setup_state(self, phone):
|
|
723
|
+
"""What Setup shows for this device. LookupError for a device Mobster doesn't set up (a WDA address)."""
|
|
724
|
+
if phone.kind == "usb":
|
|
725
|
+
if phone.primary or (phone.setup is None and not self._primary_chosen()):
|
|
726
|
+
if self.runtime.setup is None:
|
|
727
|
+
raise LookupError("Setup is managed outside this app")
|
|
728
|
+
return self.runtime.setup.state()
|
|
729
|
+
service = phone.setup if phone.setup is not None else self._loose_setup(phone)
|
|
730
|
+
return service.state()
|
|
731
|
+
if phone.kind == "simulator":
|
|
732
|
+
row = next((row for row in self._sim_rows() if row["udid"] == phone.udid), None)
|
|
733
|
+
item = self._sim_record(phone, row) if row is not None else None
|
|
734
|
+
runner = ("starting" if phone.start_state["state"] in ("starting", "stopping") else
|
|
735
|
+
"running" if item is not None and item["state"] in ("ready", "busy") else
|
|
736
|
+
"failed" if phone.start_state["state"] == "failed" else "stopped")
|
|
737
|
+
return {"kind": "simulator", "complete": runner == "running",
|
|
738
|
+
"device": {key: value for key, value in (item or {}).items() if key not in ("wdaUrl", "mjpegUrl")},
|
|
739
|
+
"runner": {"state": runner, "error": phone.start_state.get("error")}}
|
|
740
|
+
raise LookupError("Setup is managed outside this app")
|
|
741
|
+
|
|
742
|
+
def _primary_chosen(self):
|
|
743
|
+
return self._chosen() is not None
|
|
744
|
+
|
|
745
|
+
def _loose_setup(self, phone):
|
|
746
|
+
from .device_manager import DeviceManager
|
|
747
|
+
from .setup_service import SetupService
|
|
748
|
+
if self.runtime.setup is None:
|
|
749
|
+
raise LookupError("Setup is managed outside this app")
|
|
750
|
+
manager = DeviceManager(self.data_dir, registry.wda_url(0), udid=phone.udid,
|
|
751
|
+
root=registry.phone_folder(self.data_dir, phone.udid))
|
|
752
|
+
manager.cache = self.runtime.manager.cache
|
|
753
|
+
return SetupService(self.runtime, manager, self.runtime.setup.env_file, video=lambda: None,
|
|
754
|
+
busy=lambda: False)
|
|
755
|
+
|
|
756
|
+
def setup_action(self, phone, action, body):
|
|
757
|
+
if phone.kind == "usb":
|
|
758
|
+
if action == "build" and set(body) <= {"team"}:
|
|
759
|
+
target = phone if phone.primary else self.adopt(phone.udid)
|
|
760
|
+
target.manager.start_build(body.get("team"))
|
|
761
|
+
self.cache.pop("usb", None)
|
|
762
|
+
return target
|
|
763
|
+
manager = phone.manager
|
|
764
|
+
if manager is None:
|
|
765
|
+
if action == "refresh" and not body:
|
|
766
|
+
# "Check again" on a new phone's first steps (Trust, Developer Mode, the tools): read them anew.
|
|
767
|
+
self._loose_setup(phone).refresh()
|
|
768
|
+
self.cache.pop("usb", None)
|
|
769
|
+
return phone
|
|
770
|
+
raise LookupError("Set this iPhone up first: install Mobster's helper on it.")
|
|
771
|
+
if action == "start" and not body:
|
|
772
|
+
manager.start_runner()
|
|
773
|
+
elif action == "stop" and not body:
|
|
774
|
+
manager.stop_runner(remember=True)
|
|
775
|
+
elif action == "refresh" and not body:
|
|
776
|
+
(phone.setup or self.runtime.setup).refresh()
|
|
777
|
+
elif action == "photos" and not body:
|
|
778
|
+
(phone.setup or self.runtime.setup).allow_photos(phone.id)
|
|
779
|
+
else:
|
|
780
|
+
raise KeyError(action)
|
|
781
|
+
phone.health_at = 0.0
|
|
782
|
+
return phone
|
|
783
|
+
if phone.kind == "simulator":
|
|
784
|
+
if action == "start" and not body:
|
|
785
|
+
self._simulator_job(phone, "starting")
|
|
786
|
+
elif action == "stop" and not body:
|
|
787
|
+
if self.busy(phone) is not None:
|
|
788
|
+
raise LookupError("A task is running on it. Stop it first.")
|
|
789
|
+
self._simulator_job(phone, "stopping")
|
|
790
|
+
else:
|
|
791
|
+
raise KeyError(action)
|
|
792
|
+
return phone
|
|
793
|
+
raise LookupError("Setup is managed outside this app")
|
|
794
|
+
|
|
795
|
+
def _simulator_job(self, phone, kind):
|
|
796
|
+
"""Boot a simulator and start its WebDriverAgent, or shut it down, in the background."""
|
|
797
|
+
with self.lock:
|
|
798
|
+
if phone.start_state["state"] in ("starting", "stopping"):
|
|
799
|
+
return
|
|
800
|
+
phone.start_state = {"state": kind, "error": None}
|
|
801
|
+
|
|
802
|
+
def work():
|
|
803
|
+
try:
|
|
804
|
+
from .sim import SimulatorManager
|
|
805
|
+
manager = SimulatorManager(data_dir=registry.simulator_folder())
|
|
806
|
+
if kind == "starting":
|
|
807
|
+
manager.acquire_udid(phone.udid, timeout=30).release()
|
|
808
|
+
else:
|
|
809
|
+
manager.shutdown(phone.udid)
|
|
810
|
+
phone.start_state = {"state": "idle", "error": None}
|
|
811
|
+
except Exception as error:
|
|
812
|
+
phone.start_state = {"state": "failed", "error": " ".join(
|
|
813
|
+
part for part in (str(error), getattr(error, "fix", "")) if part)[:300]}
|
|
814
|
+
finally:
|
|
815
|
+
self._read_sims()
|
|
816
|
+
phone.health_at = 0.0
|
|
817
|
+
threading.Thread(target=work, name=f"mobster-sim-{kind}", daemon=True).start()
|
|
818
|
+
|
|
819
|
+
def mark_used(self, phone):
|
|
820
|
+
if phone is None or not phone.id:
|
|
821
|
+
return
|
|
822
|
+
self.last_used = phone.id
|
|
823
|
+
self.cache.setdefault("used_at", {})[phone.id] = round(time.time() * 1000)
|
|
824
|
+
if self.store is not None:
|
|
825
|
+
try:
|
|
826
|
+
self.store.set_last_used(phone.id)
|
|
827
|
+
except OSError:
|
|
828
|
+
pass
|
|
829
|
+
|
|
830
|
+
def last_used_at(self, phone):
|
|
831
|
+
return self.cache.get("used_at", {}).get(phone.id)
|
|
832
|
+
|
|
833
|
+
def videos(self):
|
|
834
|
+
return [phone._video for phone in list(self.phones.values()) if phone._video is not None]
|
|
835
|
+
|
|
836
|
+
def close(self, keep_runner=False):
|
|
837
|
+
self.closed = True
|
|
838
|
+
with self.lock:
|
|
839
|
+
phones = list(self.phones.values())
|
|
840
|
+
for phone in phones:
|
|
841
|
+
phone.close(keep_runner)
|