mobster-cli 0.4.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mobile_agent/__init__.py +4 -0
- mobile_agent/__main__.py +598 -0
- mobile_agent/accounts/__init__.py +1 -0
- mobile_agent/accounts/context.py +80 -0
- mobile_agent/accounts/mcp.py +55 -0
- mobile_agent/accounts/pacing.py +318 -0
- mobile_agent/accounts/register.py +78 -0
- mobile_agent/accounts/routes.py +212 -0
- mobile_agent/accounts/routing.py +69 -0
- mobile_agent/accounts/screen.py +273 -0
- mobile_agent/accounts/stats.py +54 -0
- mobile_agent/accounts/store.py +394 -0
- mobile_agent/agent_hooks.py +123 -0
- mobile_agent/ai_plans/__init__.py +7 -0
- mobile_agent/ai_plans/agents.py +96 -0
- mobile_agent/ai_plans/claude_code.py +208 -0
- mobile_agent/ai_plans/cli.py +400 -0
- mobile_agent/ai_plans/codex.py +258 -0
- mobile_agent/ai_plans/compat_chat.py +307 -0
- mobile_agent/ai_plans/device_login.py +164 -0
- mobile_agent/ai_plans/draft_client.py +68 -0
- mobile_agent/ai_plans/gemini.py +137 -0
- mobile_agent/ai_plans/instructions.py +65 -0
- mobile_agent/ai_plans/mcp_http.py +160 -0
- mobile_agent/ai_plans/phone_tools.py +892 -0
- mobile_agent/ai_plans/providers.py +235 -0
- mobile_agent/ai_plans/routes.py +160 -0
- mobile_agent/ai_plans/runner.py +400 -0
- mobile_agent/ai_plans/vault.py +114 -0
- mobile_agent/alerts.py +337 -0
- mobile_agent/android/__init__.py +18 -0
- mobile_agent/android/adb.py +268 -0
- mobile_agent/android/conditions.py +128 -0
- mobile_agent/android/doctor.py +54 -0
- mobile_agent/android/driver.py +137 -0
- mobile_agent/android/emulator.py +216 -0
- mobile_agent/android/logs.py +85 -0
- mobile_agent/android/manager.py +225 -0
- mobile_agent/android/records.py +31 -0
- mobile_agent/android/tree.py +96 -0
- mobile_agent/answer_shapes.py +230 -0
- mobile_agent/api_devices.py +333 -0
- mobile_agent/api_errors.py +17 -0
- mobile_agent/api_get.py +249 -0
- mobile_agent/api_http.py +467 -0
- mobile_agent/api_post.py +453 -0
- mobile_agent/api_routes.py +173 -0
- mobile_agent/app_api/__init__.py +4 -0
- mobile_agent/app_api/cli.py +151 -0
- mobile_agent/app_api/endpoints.py +193 -0
- mobile_agent/app_api/mcp.py +54 -0
- mobile_agent/app_api/openapi.py +146 -0
- mobile_agent/app_api/reading.py +172 -0
- mobile_agent/app_api/register.py +10 -0
- mobile_agent/app_api/screen.py +216 -0
- mobile_agent/app_api/service.py +382 -0
- mobile_agent/app_api/sessions.py +404 -0
- mobile_agent/app_api/tokens.py +167 -0
- mobile_agent/app_choice.py +131 -0
- mobile_agent/app_icons.py +183 -0
- mobile_agent/app_killer.py +69 -0
- mobile_agent/artifacts.py +606 -0
- mobile_agent/attach/__init__.py +13 -0
- mobile_agent/attach/cli.py +46 -0
- mobile_agent/attach/flutter.py +28 -0
- mobile_agent/attach/score.py +119 -0
- mobile_agent/attach/web.py +13 -0
- mobile_agent/attachments/__init__.py +1 -0
- mobile_agent/attachments/agent.py +418 -0
- mobile_agent/attachments/extract.py +217 -0
- mobile_agent/attachments/mcp.py +104 -0
- mobile_agent/attachments/register.py +73 -0
- mobile_agent/attachments/routes.py +326 -0
- mobile_agent/attachments/service.py +170 -0
- mobile_agent/attachments/sniff.py +152 -0
- mobile_agent/attachments/store.py +441 -0
- mobile_agent/audit_verify.py +436 -0
- mobile_agent/bench/README.md +410 -0
- mobile_agent/bench/__init__.py +0 -0
- mobile_agent/bench/__main__.py +394 -0
- mobile_agent/bench/ab.py +168 -0
- mobile_agent/bench/actuation.py +368 -0
- mobile_agent/bench/agents/__init__.py +9 -0
- mobile_agent/bench/agents/base.py +105 -0
- mobile_agent/bench/agents/plan_clients.py +245 -0
- mobile_agent/bench/agents/smart.py +533 -0
- mobile_agent/bench/checks.py +358 -0
- mobile_agent/bench/decide_gate.py +214 -0
- mobile_agent/bench/diagnose.py +181 -0
- mobile_agent/bench/evals/__init__.py +14 -0
- mobile_agent/bench/evals/build.py +565 -0
- mobile_agent/bench/evals/checkpoint.py +214 -0
- mobile_agent/bench/evals/irt.py +219 -0
- mobile_agent/bench/evals/l0.py +647 -0
- mobile_agent/bench/evals/l1.py +866 -0
- mobile_agent/bench/evals/models.py +124 -0
- mobile_agent/bench/evals/pairwise.py +189 -0
- mobile_agent/bench/evals/proxy.py +90 -0
- mobile_agent/bench/evals/sets.py +149 -0
- mobile_agent/bench/evals/traces.py +446 -0
- mobile_agent/bench/fixtures.py +176 -0
- mobile_agent/bench/guard_replay.py +129 -0
- mobile_agent/bench/h2-manifest.json +20 -0
- mobile_agent/bench/h3-manifest.json +20 -0
- mobile_agent/bench/heldout-manifest.json +32 -0
- mobile_agent/bench/heldout.py +375 -0
- mobile_agent/bench/iosworld.py +901 -0
- mobile_agent/bench/iosworld_stats.py +510 -0
- mobile_agent/bench/llm_units.py +116 -0
- mobile_agent/bench/manifest.json +93 -0
- mobile_agent/bench/metrics.py +66 -0
- mobile_agent/bench/phone.py +44 -0
- mobile_agent/bench/replay.py +172 -0
- mobile_agent/bench/report.py +207 -0
- mobile_agent/bench/runner.py +456 -0
- mobile_agent/bench/safety.py +310 -0
- mobile_agent/bench/stream_timing.py +213 -0
- mobile_agent/bench/suite.py +444 -0
- mobile_agent/bench/sweep.py +176 -0
- mobile_agent/bench/tape.py +798 -0
- mobile_agent/bench/tape_replay.py +88 -0
- mobile_agent/bench/trace_evals.py +128 -0
- mobile_agent/bench/trace_view.py +81 -0
- mobile_agent/bench/truth/iphone15pro.json +138 -0
- mobile_agent/bench/truth.py +340 -0
- mobile_agent/bench/tuning.py +543 -0
- mobile_agent/bench/tuning_analysis.py +656 -0
- mobile_agent/bench/vague-heldout-manifest.json +12 -0
- mobile_agent/bench/vague_rubric.py +135 -0
- mobile_agent/bench/vague_run.py +243 -0
- mobile_agent/brand.py +454 -0
- mobile_agent/brand_fonts.py +756 -0
- mobile_agent/build_identity.py +21 -0
- mobile_agent/cable_bt.py +623 -0
- mobile_agent/cable_bt_runtime.py +128 -0
- mobile_agent/catalog.py +69 -0
- mobile_agent/chart_svg.py +415 -0
- mobile_agent/chat_here.py +189 -0
- mobile_agent/cli_constants.py +5 -0
- mobile_agent/cli_parser.py +611 -0
- mobile_agent/cli_run.py +115 -0
- mobile_agent/compiled_read/__init__.py +21 -0
- mobile_agent/compiled_read/delta.py +89 -0
- mobile_agent/compiled_read/extractors.py +202 -0
- mobile_agent/compiled_read/routes.py +56 -0
- mobile_agent/completion.py +267 -0
- mobile_agent/compose.py +48 -0
- mobile_agent/config.py +114 -0
- mobile_agent/connectors/__init__.py +1 -0
- mobile_agent/connectors/api_session.py +76 -0
- mobile_agent/connectors/callback_page.py +146 -0
- mobile_agent/connectors/client.py +582 -0
- mobile_agent/connectors/github_app.py +55 -0
- mobile_agent/connectors/graph_api.py +396 -0
- mobile_agent/connectors/imports.py +170 -0
- mobile_agent/connectors/issue.py +66 -0
- mobile_agent/connectors/keystore.py +85 -0
- mobile_agent/connectors/marks.py +222 -0
- mobile_agent/connectors/oauth.py +476 -0
- mobile_agent/connectors/picker.py +228 -0
- mobile_agent/connectors/presets.py +591 -0
- mobile_agent/connectors/register.py +48 -0
- mobile_agent/connectors/routes.py +153 -0
- mobile_agent/connectors/service.py +966 -0
- mobile_agent/connectors/shared.py +136 -0
- mobile_agent/connectors/skill.py +446 -0
- mobile_agent/connectors/slack_api.py +269 -0
- mobile_agent/connectors/steps.py +92 -0
- mobile_agent/connectors/store.py +144 -0
- mobile_agent/connectors/vault.py +72 -0
- mobile_agent/console.py +178 -0
- mobile_agent/content_queue/__init__.py +1 -0
- mobile_agent/content_queue/client.py +263 -0
- mobile_agent/content_queue/importing.py +153 -0
- mobile_agent/content_queue/media_check.py +224 -0
- mobile_agent/content_queue/prompt.py +64 -0
- mobile_agent/content_queue/register.py +91 -0
- mobile_agent/content_queue/routes.py +370 -0
- mobile_agent/content_queue/runner.py +475 -0
- mobile_agent/content_queue/store.py +521 -0
- mobile_agent/contract.py +996 -0
- mobile_agent/contract_asks.py +248 -0
- mobile_agent/contract_prompt.py +45 -0
- mobile_agent/contract_screen.py +981 -0
- mobile_agent/contract_view.py +84 -0
- mobile_agent/corpus/__init__.py +26 -0
- mobile_agent/corpus/cli.py +63 -0
- mobile_agent/corpus/consent.py +55 -0
- mobile_agent/corpus/listener.py +76 -0
- mobile_agent/corpus/register.py +9 -0
- mobile_agent/corpus/skeleton.py +72 -0
- mobile_agent/corpus/store.py +143 -0
- mobile_agent/costs.py +161 -0
- mobile_agent/decider_scope.py +40 -0
- mobile_agent/decision_provider.py +189 -0
- mobile_agent/deeplinks.py +352 -0
- mobile_agent/device_info.py +116 -0
- mobile_agent/device_manager.py +978 -0
- mobile_agent/device_status.py +195 -0
- mobile_agent/device_targets.py +217 -0
- mobile_agent/devices.py +676 -0
- mobile_agent/devkit/__init__.py +64 -0
- mobile_agent/devkit/account_cli.py +90 -0
- mobile_agent/devkit/asc.py +562 -0
- mobile_agent/devkit/asc_cli.py +214 -0
- mobile_agent/devkit/ci.py +737 -0
- mobile_agent/devkit/ci_cli.py +196 -0
- mobile_agent/devkit/ci_hosts.py +194 -0
- mobile_agent/devkit/es256.py +197 -0
- mobile_agent/devkit/expo.py +440 -0
- mobile_agent/devkit/expo_cli.py +405 -0
- mobile_agent/devkit/expo_dev.py +203 -0
- mobile_agent/devkit/expo_routes.py +90 -0
- mobile_agent/devkit/fixtures_cli.py +133 -0
- mobile_agent/devkit/fixtures_routes.py +82 -0
- mobile_agent/devkit/links.py +375 -0
- mobile_agent/devkit/links_cli.py +123 -0
- mobile_agent/devkit/links_routes.py +83 -0
- mobile_agent/devkit/nightly.py +152 -0
- mobile_agent/devkit/shots.py +203 -0
- mobile_agent/devkit/testflight.py +155 -0
- mobile_agent/devkit/testflight_cli.py +127 -0
- mobile_agent/devkit/testflight_routes.py +111 -0
- mobile_agent/devkit/xcode.py +236 -0
- mobile_agent/devkit/xcode_cli.py +101 -0
- mobile_agent/devtools.py +419 -0
- mobile_agent/doctor.py +509 -0
- mobile_agent/doctor_fix.py +467 -0
- mobile_agent/downloads.py +55 -0
- mobile_agent/drivers.py +793 -0
- mobile_agent/dry_run.py +102 -0
- mobile_agent/effect_ledger.py +83 -0
- mobile_agent/engine_health.py +378 -0
- mobile_agent/engine_hooks.py +96 -0
- mobile_agent/engines.py +408 -0
- mobile_agent/env_file.py +63 -0
- mobile_agent/env_secrets.py +149 -0
- mobile_agent/errors.py +15 -0
- mobile_agent/evals/__init__.py +3 -0
- mobile_agent/evals/app_survey.py +149 -0
- mobile_agent/evals/development/__init__.py +1 -0
- mobile_agent/evals/development/score.py +276 -0
- mobile_agent/evals/latency_report.py +312 -0
- mobile_agent/evals/offline_report.py +374 -0
- mobile_agent/evals/oracles.py +298 -0
- mobile_agent/evals/redteam/__init__.py +15 -0
- mobile_agent/evals/redteam/__main__.py +72 -0
- mobile_agent/evals/redteam/cases.py +730 -0
- mobile_agent/evals/redteam/harness.py +604 -0
- mobile_agent/evals/redteam/live.py +139 -0
- mobile_agent/evals/redteam/plain.py +85 -0
- mobile_agent/evals/vision/__init__.py +0 -0
- mobile_agent/evals/vision/images/dog_001.jpg +0 -0
- mobile_agent/evals/vision/images/dog_002.jpg +0 -0
- mobile_agent/evals/vision/images/dog_003.jpg +0 -0
- mobile_agent/evals/vision/images/dog_004.jpg +0 -0
- mobile_agent/evals/vision/images/dog_005.jpg +0 -0
- mobile_agent/evals/vision/images/dog_006.jpg +0 -0
- mobile_agent/evals/vision/images/dog_007.jpg +0 -0
- mobile_agent/evals/vision/images/dog_010.jpg +0 -0
- mobile_agent/evals/vision/images/dog_011.jpg +0 -0
- mobile_agent/evals/vision/images/dog_013.jpg +0 -0
- mobile_agent/evals/vision/images/dog_014.jpg +0 -0
- mobile_agent/evals/vision/images/dog_015.jpg +0 -0
- mobile_agent/evals/vision/images/dog_016.jpg +0 -0
- mobile_agent/evals/vision/images/dog_017.jpg +0 -0
- mobile_agent/evals/vision/images/dog_019.jpg +0 -0
- mobile_agent/evals/vision/images/dog_020.jpg +0 -0
- mobile_agent/evals/vision/images/dog_021.jpg +0 -0
- mobile_agent/evals/vision/images/dog_022.jpg +0 -0
- mobile_agent/evals/vision/images/dog_023.jpg +0 -0
- mobile_agent/evals/vision/images/dog_024.jpg +0 -0
- mobile_agent/evals/vision/images/dog_026.jpg +0 -0
- mobile_agent/evals/vision/images/dog_027.jpg +0 -0
- mobile_agent/evals/vision/images/dog_034.jpg +0 -0
- mobile_agent/evals/vision/images/dog_035.jpg +0 -0
- mobile_agent/evals/vision/images/dog_038.jpg +0 -0
- mobile_agent/evals/vision/images/dog_042.jpg +0 -0
- mobile_agent/evals/vision/images/dog_047.jpg +0 -0
- mobile_agent/evals/vision/images/dog_052.jpg +0 -0
- mobile_agent/evals/vision/images/dog_058.jpg +0 -0
- mobile_agent/evals/vision/images/dog_062.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_000.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_010.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_011.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_015.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_016.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_017.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_019.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_021.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_025.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_026.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_027.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_030.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_032.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_033.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_034.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_035.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_036.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_037.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_039.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_040.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_041.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_042.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_043.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_044.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_046.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_047.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_048.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_049.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_051.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_053.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_054.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_055.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_058.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_002.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_003.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_004.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_007.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_009.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_010.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_011.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_012.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_013.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_014.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_016.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_017.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_023.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_027.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_029.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_032.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_033.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_036.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_041.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_043.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_047.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_049.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_051.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_053.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_056.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_057.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_058.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_060.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_061.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_062.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_063.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_064.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_065.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_066.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_067.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_068.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_069.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_070.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_071.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_000.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_001.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_002.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_004.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_005.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_008.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_009.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_010.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_012.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_013.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_015.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_016.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_017.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_018.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_021.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_022.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_023.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_024.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_030.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_031.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_032.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_033.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_034.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_036.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_038.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_039.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_040.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_041.jpg +0 -0
- mobile_agent/evals/vision/manifest.json +2355 -0
- mobile_agent/eventbus.py +110 -0
- mobile_agent/explain.py +34 -0
- mobile_agent/failure_line.py +197 -0
- mobile_agent/fanout.py +663 -0
- mobile_agent/feedback/__init__.py +2 -0
- mobile_agent/feedback/bundle.py +204 -0
- mobile_agent/feedback/outbox.py +196 -0
- mobile_agent/feedback/register.py +9 -0
- mobile_agent/feedback/routes.py +177 -0
- mobile_agent/fleet.py +841 -0
- mobile_agent/fleet_health.py +729 -0
- mobile_agent/fleet_phone.py +186 -0
- mobile_agent/frontier/__init__.py +79 -0
- mobile_agent/frontier/act.py +380 -0
- mobile_agent/frontier/actions.py +70 -0
- mobile_agent/frontier/agent.py +256 -0
- mobile_agent/frontier/approvals.py +366 -0
- mobile_agent/frontier/hooks.py +199 -0
- mobile_agent/frontier/interrupts.py +125 -0
- mobile_agent/frontier/observe.py +331 -0
- mobile_agent/frontier/phases.py +381 -0
- mobile_agent/frontier/pipeline.py +61 -0
- mobile_agent/frontier/prompt.py +384 -0
- mobile_agent/frontier/protocol.py +91 -0
- mobile_agent/frontier/safety.py +467 -0
- mobile_agent/frontier/saved.py +375 -0
- mobile_agent/frontier/screen.py +419 -0
- mobile_agent/frontier/skills.py +147 -0
- mobile_agent/frontier/stages/__init__.py +1 -0
- mobile_agent/frontier/stages/after.py +192 -0
- mobile_agent/frontier/stages/approve.py +138 -0
- mobile_agent/frontier/stages/controls.py +115 -0
- mobile_agent/frontier/stages/ends.py +102 -0
- mobile_agent/frontier/stages/gate.py +92 -0
- mobile_agent/frontier/stages/guards.py +132 -0
- mobile_agent/frontier/stages/points.py +155 -0
- mobile_agent/frontier/stages/send.py +177 -0
- mobile_agent/frontier/turn.py +151 -0
- mobile_agent/full_disk_access.py +83 -0
- mobile_agent/gemini.py +69 -0
- mobile_agent/genuine.py +120 -0
- mobile_agent/graph/__init__.py +4 -0
- mobile_agent/graph/catalog.py +789 -0
- mobile_agent/graph/chain.py +306 -0
- mobile_agent/graph/dispatch.py +98 -0
- mobile_agent/graph/draft.py +373 -0
- mobile_agent/graph/edit.py +76 -0
- mobile_agent/graph/estimate.py +189 -0
- mobile_agent/graph/expr.py +562 -0
- mobile_agent/graph/functions.py +271 -0
- mobile_agent/graph/infer.py +144 -0
- mobile_agent/graph/nodes.py +936 -0
- mobile_agent/graph/phone_steps.py +178 -0
- mobile_agent/graph/rerun.py +327 -0
- mobile_agent/graph/run.py +899 -0
- mobile_agent/graph/schema.py +842 -0
- mobile_agent/graph/state.py +161 -0
- mobile_agent/graph/values.py +198 -0
- mobile_agent/harness/__init__.py +2 -0
- mobile_agent/harness/checkpoints.py +468 -0
- mobile_agent/harness/control.py +101 -0
- mobile_agent/harness/prewarm.py +213 -0
- mobile_agent/harness/register.py +43 -0
- mobile_agent/harness/routes.py +114 -0
- mobile_agent/harness/settings.py +66 -0
- mobile_agent/harness/tools/__init__.py +67 -0
- mobile_agent/harness/tools/ask_user.py +102 -0
- mobile_agent/harness/tools/watch.py +120 -0
- mobile_agent/harness_api.py +815 -0
- mobile_agent/hedge.py +203 -0
- mobile_agent/help_topics.py +305 -0
- mobile_agent/hints.py +248 -0
- mobile_agent/history.py +69 -0
- mobile_agent/home.py +214 -0
- mobile_agent/http_pool.py +175 -0
- mobile_agent/image_policy.py +139 -0
- mobile_agent/inbox.py +148 -0
- mobile_agent/installed_apps.py +344 -0
- mobile_agent/integrations/__init__.py +1 -0
- mobile_agent/integrations/cli.py +472 -0
- mobile_agent/integrations/client_files.py +65 -0
- mobile_agent/integrations/clients.py +998 -0
- mobile_agent/integrations/doctor.py +185 -0
- mobile_agent/integrations/jsonc.py +383 -0
- mobile_agent/integrations/routes.py +135 -0
- mobile_agent/integrations/rules.py +112 -0
- mobile_agent/integrations/server_options.py +46 -0
- mobile_agent/integrations/skill.py +152 -0
- mobile_agent/integrations/skill_files.py +10 -0
- mobile_agent/integrations/textedit.py +309 -0
- mobile_agent/intents/__init__.py +17 -0
- mobile_agent/intents/atlas.py +216 -0
- mobile_agent/intents/callback.py +44 -0
- mobile_agent/intents/cli.py +55 -0
- mobile_agent/intents/harvest.py +91 -0
- mobile_agent/intents/plan.py +93 -0
- mobile_agent/ios/__init__.py +1 -0
- mobile_agent/ios/guide.py +16 -0
- mobile_agent/ios/kb.py +212 -0
- mobile_agent/ios/kb_data.py +1588 -0
- mobile_agent/ios/mcp.py +44 -0
- mobile_agent/ios/register.py +8 -0
- mobile_agent/ios/system.py +369 -0
- mobile_agent/journal.py +531 -0
- mobile_agent/keychain_acl.py +374 -0
- mobile_agent/keys.py +395 -0
- mobile_agent/latency_overlap.py +36 -0
- mobile_agent/latency_trace.py +170 -0
- mobile_agent/learned/__init__.py +15 -0
- mobile_agent/learned/app_path.py +160 -0
- mobile_agent/learned/code_read.py +38 -0
- mobile_agent/learned/parts.py +546 -0
- mobile_agent/learned/programs.py +691 -0
- mobile_agent/learned/register.py +171 -0
- mobile_agent/learned/routes.py +61 -0
- mobile_agent/learned/taught.py +329 -0
- mobile_agent/learned/teach.py +538 -0
- mobile_agent/learned/workflows.py +647 -0
- mobile_agent/learned/zero.py +41 -0
- mobile_agent/links.py +37 -0
- mobile_agent/listen.py +607 -0
- mobile_agent/lock_spike.py +278 -0
- mobile_agent/lockscreen.py +526 -0
- mobile_agent/login.py +354 -0
- mobile_agent/longform/__init__.py +15 -0
- mobile_agent/longform/ads.py +191 -0
- mobile_agent/longform/blocks.py +117 -0
- mobile_agent/longform/budget.py +265 -0
- mobile_agent/longform/context.py +285 -0
- mobile_agent/longform/costs.py +93 -0
- mobile_agent/longform/digest.py +145 -0
- mobile_agent/longform/extract.py +508 -0
- mobile_agent/longform/feed.py +505 -0
- mobile_agent/longform/followup.py +168 -0
- mobile_agent/longform/frames.py +82 -0
- mobile_agent/longform/identity.py +87 -0
- mobile_agent/longform/inbox.py +221 -0
- mobile_agent/longform/items.py +425 -0
- mobile_agent/longform/models.py +115 -0
- mobile_agent/longform/notebook.py +329 -0
- mobile_agent/longform/plan.py +614 -0
- mobile_agent/longform/recall.py +195 -0
- mobile_agent/longform/receipts.py +81 -0
- mobile_agent/longform/register.py +124 -0
- mobile_agent/longform/report.py +343 -0
- mobile_agent/longform/routes.py +102 -0
- mobile_agent/longform/session.py +644 -0
- mobile_agent/longform/store.py +146 -0
- mobile_agent/longform/synthesis.py +347 -0
- mobile_agent/mac_tools.py +223 -0
- mobile_agent/manual_control.py +143 -0
- mobile_agent/maps.py +149 -0
- mobile_agent/marks.py +92 -0
- mobile_agent/mcp_server/__init__.py +3 -0
- mobile_agent/mcp_server/__main__.py +29 -0
- mobile_agent/mcp_server/action_journal.py +24 -0
- mobile_agent/mcp_server/action_tools.py +638 -0
- mobile_agent/mcp_server/approvals.py +219 -0
- mobile_agent/mcp_server/catalog.py +92 -0
- mobile_agent/mcp_server/cli.py +194 -0
- mobile_agent/mcp_server/codes.py +147 -0
- mobile_agent/mcp_server/device_tools.py +306 -0
- mobile_agent/mcp_server/direct.py +272 -0
- mobile_agent/mcp_server/expo_tools.py +105 -0
- mobile_agent/mcp_server/phone_tools.py +510 -0
- mobile_agent/mcp_server/protocol.py +364 -0
- mobile_agent/mcp_server/registry.py +55 -0
- mobile_agent/mcp_server/run_tools.py +531 -0
- mobile_agent/mcp_server/schemas.py +425 -0
- mobile_agent/mcp_server/screen_tools.py +168 -0
- mobile_agent/mcp_server/session.py +469 -0
- mobile_agent/mcp_server/support.py +145 -0
- mobile_agent/mcp_server/tools.py +493 -0
- mobile_agent/memory/__init__.py +9 -0
- mobile_agent/memory/cli.py +207 -0
- mobile_agent/memory/extract.py +191 -0
- mobile_agent/memory/listener.py +100 -0
- mobile_agent/memory/model.py +144 -0
- mobile_agent/memory/project.py +96 -0
- mobile_agent/memory/provider.py +127 -0
- mobile_agent/memory/register.py +21 -0
- mobile_agent/memory/retrieval.py +172 -0
- mobile_agent/memory/routes.py +216 -0
- mobile_agent/memory/runs.py +124 -0
- mobile_agent/memory/store.py +806 -0
- mobile_agent/memory/writer.py +278 -0
- mobile_agent/mirroring.py +585 -0
- mobile_agent/mirroring_runtime.py +221 -0
- mobile_agent/model_clients.py +604 -0
- mobile_agent/narrate.py +813 -0
- mobile_agent/native_helpers.py +84 -0
- mobile_agent/netxray/__init__.py +1 -0
- mobile_agent/netxray/analyze.py +322 -0
- mobile_agent/netxray/attribution.py +62 -0
- mobile_agent/netxray/capture.py +88 -0
- mobile_agent/netxray/cli.py +142 -0
- mobile_agent/netxray/job.py +219 -0
- mobile_agent/netxray/mobster-pcap.c +560 -0
- mobile_agent/netxray/packets.py +410 -0
- mobile_agent/netxray/pcapng.py +223 -0
- mobile_agent/netxray/register.py +93 -0
- mobile_agent/netxray/report.py +214 -0
- mobile_agent/netxray/store.py +48 -0
- mobile_agent/netxray/summary.py +157 -0
- mobile_agent/netxray/target.py +67 -0
- mobile_agent/netxray/tool.py +66 -0
- mobile_agent/netxray/trackers.py +194 -0
- mobile_agent/notify_triggers/__init__.py +1 -0
- mobile_agent/notify_triggers/rules.py +39 -0
- mobile_agent/notify_triggers/sources.py +315 -0
- mobile_agent/notify_triggers/watch.py +135 -0
- mobile_agent/offers.py +86 -0
- mobile_agent/orchestrate.py +215 -0
- mobile_agent/outlets/__init__.py +1 -0
- mobile_agent/outlets/connect.py +249 -0
- mobile_agent/outlets/folder.py +180 -0
- mobile_agent/outlets/hooks.py +453 -0
- mobile_agent/outlets/inbound.py +472 -0
- mobile_agent/outlets/inbound_reply.py +57 -0
- mobile_agent/outlets/inbound_routes.py +81 -0
- mobile_agent/outlets/kinds.py +308 -0
- mobile_agent/outlets/mail.py +110 -0
- mobile_agent/outlets/net.py +86 -0
- mobile_agent/outlets/notion.py +244 -0
- mobile_agent/outlets/register.py +56 -0
- mobile_agent/outlets/routes.py +135 -0
- mobile_agent/outlets/service.py +580 -0
- mobile_agent/outlets/sheets.py +169 -0
- mobile_agent/outlets/slack.py +177 -0
- mobile_agent/outlets/store.py +194 -0
- mobile_agent/outlets/vault.py +80 -0
- mobile_agent/outlets/webhook.py +316 -0
- mobile_agent/output_contract.py +141 -0
- mobile_agent/owner_presence.py +211 -0
- mobile_agent/ownership.py +219 -0
- mobile_agent/packs/__init__.py +1 -0
- mobile_agent/packs/calendar.py +243 -0
- mobile_agent/packs/context.py +68 -0
- mobile_agent/packs/core.py +757 -0
- mobile_agent/packs/front_door.py +316 -0
- mobile_agent/packs/heal.py +214 -0
- mobile_agent/packs/health.py +122 -0
- mobile_agent/packs/insights.py +76 -0
- mobile_agent/packs/instagram.py +640 -0
- mobile_agent/packs/linkedin.py +106 -0
- mobile_agent/packs/maps.py +196 -0
- mobile_agent/packs/mcp.py +149 -0
- mobile_agent/packs/messages.py +270 -0
- mobile_agent/packs/notes.py +226 -0
- mobile_agent/packs/photos.py +166 -0
- mobile_agent/packs/pins.py +47 -0
- mobile_agent/packs/reddit.py +110 -0
- mobile_agent/packs/register.py +276 -0
- mobile_agent/packs/rows.py +108 -0
- mobile_agent/packs/safari.py +143 -0
- mobile_agent/packs/settings.py +253 -0
- mobile_agent/packs/social.py +300 -0
- mobile_agent/packs/threads.py +97 -0
- mobile_agent/packs/tiktok.py +455 -0
- mobile_agent/packs/tools.py +290 -0
- mobile_agent/packs/x.py +127 -0
- mobile_agent/packs/youtube.py +199 -0
- mobile_agent/parity.py +270 -0
- mobile_agent/paths.py +99 -0
- mobile_agent/perception.py +470 -0
- mobile_agent/permissions/__init__.py +32 -0
- mobile_agent/permissions/consequence.py +535 -0
- mobile_agent/permissions/exceptions.py +99 -0
- mobile_agent/permissions/gate.py +316 -0
- mobile_agent/permissions/outside.py +316 -0
- mobile_agent/permissions/policy.py +288 -0
- mobile_agent/permissions/project_policy.py +411 -0
- mobile_agent/permissions/register.py +148 -0
- mobile_agent/permissions/rules.py +291 -0
- mobile_agent/phone_io/__init__.py +146 -0
- mobile_agent/phone_io/cli.py +427 -0
- mobile_agent/phone_io/clipboard.py +78 -0
- mobile_agent/phone_io/controls.py +78 -0
- mobile_agent/phone_io/files.py +289 -0
- mobile_agent/phone_io/handoff.py +123 -0
- mobile_agent/phone_io/icloud.py +118 -0
- mobile_agent/phone_io/install.py +188 -0
- mobile_agent/phone_io/network_capture.py +188 -0
- mobile_agent/phone_io/network_cli.py +138 -0
- mobile_agent/phone_io/network_profiles.py +138 -0
- mobile_agent/phone_io/photos.py +512 -0
- mobile_agent/phone_io/recording.py +115 -0
- mobile_agent/phone_io/secrets.py +146 -0
- mobile_agent/phone_io/transfer.py +895 -0
- mobile_agent/phone_io/tunnel.py +129 -0
- mobile_agent/phone_ready.py +91 -0
- mobile_agent/pinning.py +158 -0
- mobile_agent/plan_gate.py +807 -0
- mobile_agent/plugins/__init__.py +3 -0
- mobile_agent/plugins/install.py +163 -0
- mobile_agent/plugins/manifest.py +242 -0
- mobile_agent/plugins/register.py +15 -0
- mobile_agent/plugins/routes.py +82 -0
- mobile_agent/plugins/service.py +322 -0
- mobile_agent/plugins/skills.py +76 -0
- mobile_agent/plugins/store.py +79 -0
- mobile_agent/pool.py +285 -0
- mobile_agent/power_watch.py +275 -0
- mobile_agent/premium_model.py +22 -0
- mobile_agent/project_env.py +291 -0
- mobile_agent/project_env_cli.py +74 -0
- mobile_agent/project_env_routes.py +55 -0
- mobile_agent/projects/__init__.py +27 -0
- mobile_agent/projects/ci.py +332 -0
- mobile_agent/projects/clone.py +339 -0
- mobile_agent/projects/drafts.py +158 -0
- mobile_agent/projects/folder.py +436 -0
- mobile_agent/projects/github.py +131 -0
- mobile_agent/projects/register.py +31 -0
- mobile_agent/projects/revisions.py +253 -0
- mobile_agent/projects/routes.py +234 -0
- mobile_agent/projects/runs.py +297 -0
- mobile_agent/projects/service.py +695 -0
- mobile_agent/projects/setup.py +84 -0
- mobile_agent/projects/store.py +379 -0
- mobile_agent/projects/sync.py +380 -0
- mobile_agent/projects/workflow_files.py +270 -0
- mobile_agent/replay_core/__init__.py +26 -0
- mobile_agent/replay_core/checks.py +594 -0
- mobile_agent/replay_core/engine.py +596 -0
- mobile_agent/replay_core/heal.py +364 -0
- mobile_agent/replay_core/path.py +240 -0
- mobile_agent/replay_core/recorder.py +206 -0
- mobile_agent/replay_core/store.py +171 -0
- mobile_agent/result_blocks.py +941 -0
- mobile_agent/run.py +642 -0
- mobile_agent/run_delivery.py +123 -0
- mobile_agent/run_executor.py +200 -0
- mobile_agent/run_export.py +284 -0
- mobile_agent/run_gif.py +891 -0
- mobile_agent/runner_hints.py +86 -0
- mobile_agent/runner_renewal.py +205 -0
- mobile_agent/runtime_create.py +549 -0
- mobile_agent/runtime_driver.py +458 -0
- mobile_agent/runtime_limits.py +319 -0
- mobile_agent/runtime_queue.py +394 -0
- mobile_agent/runtime_runs.py +383 -0
- mobile_agent/runtime_status.py +181 -0
- mobile_agent/runtime_teach.py +295 -0
- mobile_agent/runtime_work.py +599 -0
- mobile_agent/screen_text.py +101 -0
- mobile_agent/secret_filter.py +113 -0
- mobile_agent/serve.py +160 -0
- mobile_agent/server.py +363 -0
- mobile_agent/service_support.py +42 -0
- mobile_agent/settle/__init__.py +1 -0
- mobile_agent/settle/predict.py +102 -0
- mobile_agent/settle/replay.py +92 -0
- mobile_agent/settle/store.py +93 -0
- mobile_agent/settle/stream.py +43 -0
- mobile_agent/setup_cli.py +225 -0
- mobile_agent/setup_errors.py +87 -0
- mobile_agent/setup_service.py +527 -0
- mobile_agent/share.py +104 -0
- mobile_agent/signing.py +184 -0
- mobile_agent/sim/__init__.py +6 -0
- mobile_agent/sim/__main__.py +26 -0
- mobile_agent/sim/api.py +952 -0
- mobile_agent/sim/apps.py +172 -0
- mobile_agent/sim/biometrics.py +84 -0
- mobile_agent/sim/cli.py +271 -0
- mobile_agent/sim/doctor.py +293 -0
- mobile_agent/sim/governor.py +90 -0
- mobile_agent/sim/holders.py +88 -0
- mobile_agent/sim/models.py +62 -0
- mobile_agent/sim/registry.py +161 -0
- mobile_agent/sim/simctl.py +269 -0
- mobile_agent/sim/wda.py +618 -0
- mobile_agent/sim/xcode.py +74 -0
- mobile_agent/skills/__init__.py +12 -0
- mobile_agent/skills/codes.py +597 -0
- mobile_agent/skills/notifications.py +281 -0
- mobile_agent/skills/open_url.py +127 -0
- mobile_agent/smart_events.py +588 -0
- mobile_agent/smart_result.py +490 -0
- mobile_agent/smart_run.py +413 -0
- mobile_agent/state.py +662 -0
- mobile_agent/states.py +217 -0
- mobile_agent/steering.py +70 -0
- mobile_agent/style.py +178 -0
- mobile_agent/supervised.py +234 -0
- mobile_agent/swift/helper_bt_hid.swift +534 -0
- mobile_agent/swift/helper_doc_text.swift +184 -0
- mobile_agent/swift/helper_draft_reply.swift +170 -0
- mobile_agent/swift/helper_map.swift +110 -0
- mobile_agent/swift/helper_mirroring.swift +1099 -0
- mobile_agent/swift/helper_mjpeg_mp4.swift +103 -0
- mobile_agent/swift/helper_pdf.swift +100 -0
- mobile_agent/swift/helper_phone_audio.swift +993 -0
- mobile_agent/swift/helper_phone_screen.swift +416 -0
- mobile_agent/swift/helper_screen_text.swift +24 -0
- mobile_agent/swift/helper_transfer.swift +246 -0
- mobile_agent/swift/ocr.swift +22 -0
- mobile_agent/task_policy.py +337 -0
- mobile_agent/team_signing/__init__.py +34 -0
- mobile_agent/team_signing/apple_ca.py +37 -0
- mobile_agent/team_signing/bundle.py +136 -0
- mobile_agent/team_signing/cli.py +208 -0
- mobile_agent/team_signing/identity.py +248 -0
- mobile_agent/team_signing/ledger.py +105 -0
- mobile_agent/team_signing/prebuilt.py +61 -0
- mobile_agent/team_signing/resign.py +256 -0
- mobile_agent/team_signing/service.py +382 -0
- mobile_agent/team_signing/tooling.py +243 -0
- mobile_agent/team_signing/writes.py +268 -0
- mobile_agent/terminal_image.py +81 -0
- mobile_agent/testkit/__init__.py +1 -0
- mobile_agent/testkit/a11y_baseline.py +132 -0
- mobile_agent/testkit/api.py +403 -0
- mobile_agent/testkit/capsule.py +400 -0
- mobile_agent/testkit/capsule_cli.py +57 -0
- mobile_agent/testkit/cli.py +948 -0
- mobile_agent/testkit/cli_arguments.py +193 -0
- mobile_agent/testkit/comparison.py +288 -0
- mobile_agent/testkit/comparison_cli.py +41 -0
- mobile_agent/testkit/discover.py +257 -0
- mobile_agent/testkit/editor.py +243 -0
- mobile_agent/testkit/execute.py +515 -0
- mobile_agent/testkit/github_checks.py +248 -0
- mobile_agent/testkit/gitref.py +66 -0
- mobile_agent/testkit/heal_review.py +176 -0
- mobile_agent/testkit/health.py +480 -0
- mobile_agent/testkit/history.py +255 -0
- mobile_agent/testkit/hosted_results.py +321 -0
- mobile_agent/testkit/input_files.py +173 -0
- mobile_agent/testkit/jobs.py +285 -0
- mobile_agent/testkit/junit_verdict.py +33 -0
- mobile_agent/testkit/maestro.py +802 -0
- mobile_agent/testkit/mcp.py +562 -0
- mobile_agent/testkit/mcp_comparison.py +88 -0
- mobile_agent/testkit/minimize.py +471 -0
- mobile_agent/testkit/minimizer_cli.py +48 -0
- mobile_agent/testkit/notify.py +212 -0
- mobile_agent/testkit/phone_video.py +191 -0
- mobile_agent/testkit/pr.py +491 -0
- mobile_agent/testkit/pr_report.py +562 -0
- mobile_agent/testkit/prbuild.py +148 -0
- mobile_agent/testkit/proof_report.py +55 -0
- mobile_agent/testkit/provenance.py +119 -0
- mobile_agent/testkit/prrun.py +244 -0
- mobile_agent/testkit/recent.py +166 -0
- mobile_agent/testkit/recipes.py +146 -0
- mobile_agent/testkit/record.py +204 -0
- mobile_agent/testkit/register.py +288 -0
- mobile_agent/testkit/reports.py +998 -0
- mobile_agent/testkit/repro.py +432 -0
- mobile_agent/testkit/scenario.py +440 -0
- mobile_agent/testkit/scenario_cli.py +37 -0
- mobile_agent/testkit/snapshots_cli.py +60 -0
- mobile_agent/testkit/suite.py +670 -0
- mobile_agent/testkit/targets.py +497 -0
- mobile_agent/testkit/xctest.py +378 -0
- mobile_agent/threads/__init__.py +1 -0
- mobile_agent/threads/chat_once.py +264 -0
- mobile_agent/threads/cli.py +740 -0
- mobile_agent/threads/client.py +326 -0
- mobile_agent/threads/context.py +246 -0
- mobile_agent/threads/mcp.py +739 -0
- mobile_agent/threads/recent.py +34 -0
- mobile_agent/threads/register.py +54 -0
- mobile_agent/threads/routes.py +188 -0
- mobile_agent/threads/service.py +975 -0
- mobile_agent/threads/store.py +361 -0
- mobile_agent/threads/summary.py +117 -0
- mobile_agent/threads/term.py +101 -0
- mobile_agent/threads/workflow_cli.py +104 -0
- mobile_agent/tls.py +36 -0
- mobile_agent/traces.py +271 -0
- mobile_agent/tracks.py +235 -0
- mobile_agent/transport.py +271 -0
- mobile_agent/triggers/__init__.py +1 -0
- mobile_agent/triggers/actions.py +204 -0
- mobile_agent/triggers/model.py +146 -0
- mobile_agent/triggers/register.py +29 -0
- mobile_agent/triggers/routes.py +101 -0
- mobile_agent/triggers/service.py +387 -0
- mobile_agent/triggers/store.py +142 -0
- mobile_agent/tui/__init__.py +89 -0
- mobile_agent/tui/app.py +582 -0
- mobile_agent/tui/chrome.py +321 -0
- mobile_agent/tui/commands.py +375 -0
- mobile_agent/tui/demo_phone.py +369 -0
- mobile_agent/tui/images.py +29 -0
- mobile_agent/tui/keys.py +64 -0
- mobile_agent/tui/prompt.py +195 -0
- mobile_agent/tui/render.py +395 -0
- mobile_agent/tui/screens.py +180 -0
- mobile_agent/tui/session.py +308 -0
- mobile_agent/tui/tasks.py +287 -0
- mobile_agent/tui/theme.py +118 -0
- mobile_agent/tui/widgets.py +272 -0
- mobile_agent/update_check.py +213 -0
- mobile_agent/usage.py +184 -0
- mobile_agent/verify/__init__.py +0 -0
- mobile_agent/verify/__main__.py +41 -0
- mobile_agent/verify/a11y.py +189 -0
- mobile_agent/verify/actions.py +290 -0
- mobile_agent/verify/assertions.py +570 -0
- mobile_agent/verify/audit.py +312 -0
- mobile_agent/verify/business.py +221 -0
- mobile_agent/verify/business_run.py +131 -0
- mobile_agent/verify/business_state.py +168 -0
- mobile_agent/verify/check_schema.py +398 -0
- mobile_agent/verify/checks.py +614 -0
- mobile_agent/verify/cli.py +423 -0
- mobile_agent/verify/conditions.py +483 -0
- mobile_agent/verify/crashes.py +321 -0
- mobile_agent/verify/device_audit.py +246 -0
- mobile_agent/verify/dialogs.py +287 -0
- mobile_agent/verify/evidence.py +705 -0
- mobile_agent/verify/fixtures.py +305 -0
- mobile_agent/verify/flow.py +213 -0
- mobile_agent/verify/l10n.py +141 -0
- mobile_agent/verify/netcerts.py +67 -0
- mobile_agent/verify/netproxy.py +356 -0
- mobile_agent/verify/netrun.py +81 -0
- mobile_agent/verify/netshim.m +28 -0
- mobile_agent/verify/netshim.py +48 -0
- mobile_agent/verify/netstubs.py +132 -0
- mobile_agent/verify/overlay.py +174 -0
- mobile_agent/verify/perf.py +239 -0
- mobile_agent/verify/proof.py +192 -0
- mobile_agent/verify/replay_admission.py +48 -0
- mobile_agent/verify/report.py +952 -0
- mobile_agent/verify/run_cleanup.py +67 -0
- mobile_agent/verify/run_links.py +48 -0
- mobile_agent/verify/run_perf.py +44 -0
- mobile_agent/verify/run_secrets.py +39 -0
- mobile_agent/verify/runner.py +976 -0
- mobile_agent/verify/runs.py +57 -0
- mobile_agent/verify/screen_coverage.py +181 -0
- mobile_agent/verify/smart.py +106 -0
- mobile_agent/verify/snapshot_run.py +114 -0
- mobile_agent/verify/snapshots.py +306 -0
- mobile_agent/verify/sound.py +335 -0
- mobile_agent/verify/source_observation.py +130 -0
- mobile_agent/verify/system_surfaces/__init__.py +7 -0
- mobile_agent/verify/system_surfaces/play.py +106 -0
- mobile_agent/verify/system_surfaces/spec.py +139 -0
- mobile_agent/verify/system_surfaces/stage.py +138 -0
- mobile_agent/verify/system_surfaces/surfaces.py +121 -0
- mobile_agent/verify/tree.py +223 -0
- mobile_agent/verify/verdict.py +377 -0
- mobile_agent/verify/wire.py +392 -0
- mobile_agent/verify/wirestore.py +176 -0
- mobile_agent/waiting/__init__.py +1 -0
- mobile_agent/waiting/contacts.py +73 -0
- mobile_agent/waiting/drafts.py +136 -0
- mobile_agent/waiting/job.py +145 -0
- mobile_agent/waiting/messages.py +174 -0
- mobile_agent/waiting/rank.py +115 -0
- mobile_agent/waiting/register.py +26 -0
- mobile_agent/watch.py +502 -0
- mobile_agent/wda/__init__.py +2 -0
- mobile_agent/wda/frame_clock.py +646 -0
- mobile_agent/wda/frame_source.py +325 -0
- mobile_agent/wda/frame_store.py +115 -0
- mobile_agent/wda/gate.py +163 -0
- mobile_agent/wda/gestures.py +291 -0
- mobile_agent/wda/guards.py +94 -0
- mobile_agent/wda/identity.py +55 -0
- mobile_agent/wda/observe.py +884 -0
- mobile_agent/wda/patches.py +856 -0
- mobile_agent/wda/point_gestures.py +150 -0
- mobile_agent/wda/source.py +32 -0
- mobile_agent/wda/system_dialogs.py +571 -0
- mobile_agent/wda/text.py +694 -0
- mobile_agent/wda/timeouts.py +77 -0
- mobile_agent/wda/video.py +505 -0
- mobile_agent/wda/watchdog.py +243 -0
- mobile_agent/wireless/__init__.py +15 -0
- mobile_agent/wireless/api.py +354 -0
- mobile_agent/wireless/cli.py +287 -0
- mobile_agent/wireless/devicectl.py +229 -0
- mobile_agent/wireless/exposure.py +33 -0
- mobile_agent/wireless/mobster-wifi-pair.c +122 -0
- mobile_agent/wireless/monitor.py +332 -0
- mobile_agent/wireless/network.py +334 -0
- mobile_agent/wireless/onboarding.py +293 -0
- mobile_agent/wireless/pair.py +124 -0
- mobile_agent/wireless/probe.py +322 -0
- mobile_agent/wireless/register.py +8 -0
- mobile_agent/wireless/relay.py +179 -0
- mobile_agent/wireless/result.py +87 -0
- mobile_agent/wireless/store.py +85 -0
- mobile_agent/wireless/switch.py +77 -0
- mobile_agent/wireless/transport.py +118 -0
- mobile_agent/wireless/words.py +81 -0
- mobile_agent/wireless/xctestrun.py +83 -0
- mobile_agent/workflow_tools.py +59 -0
- mobile_agent/workflows/__init__.py +2 -0
- mobile_agent/workflows/brief.py +201 -0
- mobile_agent/workflows/cron.py +110 -0
- mobile_agent/workflows/health.py +257 -0
- mobile_agent/workflows/history.py +126 -0
- mobile_agent/workflows/records.py +442 -0
- mobile_agent/workflows/runs.py +1000 -0
- mobile_agent/workflows/scheduler.py +134 -0
- mobile_agent/workflows/service.py +973 -0
- mobile_agent/workflows/starters.py +285 -0
- mobile_agent/workflows/triggers.py +248 -0
- mobile_agent/xray/__init__.py +1 -0
- mobile_agent/xray/app_cli.py +116 -0
- mobile_agent/xray/cli.py +21 -0
- mobile_agent/xray/crawl.py +754 -0
- mobile_agent/xray/diff.py +171 -0
- mobile_agent/xray/diff_cli.py +43 -0
- mobile_agent/xray/job.py +184 -0
- mobile_agent/xray/paywall.py +188 -0
- mobile_agent/xray/policy.py +305 -0
- mobile_agent/xray/register.py +43 -0
- mobile_agent/xray/report.py +283 -0
- mobile_agent/xray/screens.py +117 -0
- mobster_cli-0.4.3.dist-info/METADATA +93 -0
- mobster_cli-0.4.3.dist-info/RECORD +1009 -0
- mobster_cli-0.4.3.dist-info/WHEEL +5 -0
- mobster_cli-0.4.3.dist-info/entry_points.txt +3 -0
- mobster_cli-0.4.3.dist-info/licenses/LICENSE +21 -0
- mobster_cli-0.4.3.dist-info/top_level.txt +1 -0
mobile_agent/sim/api.py
ADDED
|
@@ -0,0 +1,952 @@
|
|
|
1
|
+
"""The simulator manager (SPEC §12.1): headless simulators Mobster creates, boots and keeps warm."""
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
import os
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Callable, Optional
|
|
9
|
+
from urllib.parse import urlsplit
|
|
10
|
+
|
|
11
|
+
from ..devices import LOOPBACK, wda_url
|
|
12
|
+
from . import apps, holders, simctl, xcode
|
|
13
|
+
from .governor import MAX_SIMS_CEILING, SIM_GB, Governor
|
|
14
|
+
from .models import SIM_KINDS, AppInfo, SimError, SimLease, SimTarget # noqa: F401 (SIM_KINDS: api is the contract)
|
|
15
|
+
from .registry import Registry, free_slot, now, port_base, taken_ports
|
|
16
|
+
from .wda import MJPEG_WAIT, MOVE, RESTART, REUSE, SimulatorWDA, WebDriverAgent, build_dir, find_xctestrun, heartbeat
|
|
17
|
+
|
|
18
|
+
# The settings _configure writes after a boot, and again whenever the runner (re)starts.
|
|
19
|
+
KEYBOARD_DEFAULTS = ("KeyboardAutocorrection", "KeyboardPrediction", "SmartQuotesEnabled", "SmartDashesEnabled")
|
|
20
|
+
KEYBOARD_PREFERENCES = "com.apple.keyboard.preferences"
|
|
21
|
+
# The slide-typing introduction's Continue button is in no app's tree, so it is marked as already shown.
|
|
22
|
+
KEYBOARD_INTRO = (("DidShowContinuousPathIntroduction", "true"), ("DidShowGestureKeyboardIntroduction", "true"),
|
|
23
|
+
("KeyboardDidShowProductivityTutorial", "true"), ("KeyboardContinuousPathEnabled", "false"))
|
|
24
|
+
STATUS_BAR = ("--time", "9:41", "--dataNetwork", "wifi", "--wifiBars", "3", "--cellularBars", "4",
|
|
25
|
+
"--batteryState", "charged", "--batteryLevel", "100")
|
|
26
|
+
BOOT_TIMEOUT = 240
|
|
27
|
+
OPEN_PROMPT = "Open in" # iOS 26's "Open in “<App>”?" after `simctl openurl` with a custom scheme
|
|
28
|
+
OPEN_BUTTON = "Open"
|
|
29
|
+
# iOS remembers an Open in com.apple.launchservices.schemeapproval as "<opener>--><scheme>" = <bundle id>, and
|
|
30
|
+
# `simctl openurl` opens links as CoreSimulatorBridge. Writing that answer when Mobster installs an app means
|
|
31
|
+
# its own links never stop at the prompt (measured on iOS 26.4, 28 Sep).
|
|
32
|
+
SCHEME_APPROVALS = "com.apple.launchservices.schemeapproval"
|
|
33
|
+
URL_OPENER = "com.apple.CoreSimulator.CoreSimulatorBridge"
|
|
34
|
+
OPEN_PROMPT_WAIT = 3.0 # how long open_url looks for that prompt, in reads the runner answered
|
|
35
|
+
OPEN_PROMPT_LIMIT = 30.0 # and at most, when the runner is slow to answer
|
|
36
|
+
ALERT_GONE_WAIT = 2.0 # how long an answered alert has to leave the screen
|
|
37
|
+
ALERT_POLL = 0.2
|
|
38
|
+
SWEEP_ROUNDS = 3 # alerts dismissed in a row before a simulator is handed out
|
|
39
|
+
DEFAULT_MAX_SIMS = MAX_SIMS_CEILING
|
|
40
|
+
PRUNE_DAYS = 14
|
|
41
|
+
BUSY_POLL = 1.0
|
|
42
|
+
PROBE_RETRY = 0.2 # before creating another simulator: a list() probe holds a lease for milliseconds
|
|
43
|
+
XCRUN = simctl.XCRUN
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _device_lease(wda_url):
|
|
47
|
+
from ..journal import Lease
|
|
48
|
+
return Lease.device(wda_url)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _port(url):
|
|
52
|
+
return urlsplit(url).port
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _parse_time(text):
|
|
56
|
+
try:
|
|
57
|
+
return datetime.datetime.strptime(str(text), "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=datetime.timezone.utc)
|
|
58
|
+
except (TypeError, ValueError):
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class SimulatorManager:
|
|
63
|
+
"""Creates, boots and keeps Mobster's simulators, and runs the app under test on them."""
|
|
64
|
+
|
|
65
|
+
# Mobster's own simulators, never someone's phone: verify.smart.is_real_device treats a manager that doesn't
|
|
66
|
+
# say as a phone.
|
|
67
|
+
real_device = False
|
|
68
|
+
|
|
69
|
+
def __init__(self, data_dir: Optional[Path] = None, progress: Optional[Callable[[str], None]] = None):
|
|
70
|
+
from ..paths import dev_data_dir
|
|
71
|
+
self.data_dir = Path(data_dir).expanduser() if data_dir else dev_data_dir()
|
|
72
|
+
# also called from the thread that builds WebDriverAgent while a simulator boots: it must be thread-safe
|
|
73
|
+
self.progress = progress or (lambda message: None)
|
|
74
|
+
self.registry = Registry(self.data_dir)
|
|
75
|
+
self.run = simctl.run
|
|
76
|
+
self.wda = WebDriverAgent(self.data_dir, log=self._log, progress=self._say)
|
|
77
|
+
self.lease = _device_lease
|
|
78
|
+
self.sleep = time.sleep
|
|
79
|
+
self.clock = time.monotonic
|
|
80
|
+
self._tools = None
|
|
81
|
+
self._approved = {} # udid -> {scheme}: links this manager knows open without the prompt
|
|
82
|
+
self.governor = Governor()
|
|
83
|
+
# who is asking, for the holder record `mobster sim list` shows: {"client": ..., "session": ...}
|
|
84
|
+
self.who = lambda: {"client": "mobster", "session": f"pid {os.getpid()}"}
|
|
85
|
+
|
|
86
|
+
# -- plumbing ------------------------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
def _say(self, message):
|
|
89
|
+
self._log(message)
|
|
90
|
+
try:
|
|
91
|
+
self.progress(message)
|
|
92
|
+
except Exception: # a progress sink must never break a run
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
def _log(self, message):
|
|
96
|
+
try:
|
|
97
|
+
folder = self.data_dir / "logs"
|
|
98
|
+
folder.mkdir(parents=True, exist_ok=True)
|
|
99
|
+
with open(folder / "sim.log", "a", encoding="utf-8") as stream:
|
|
100
|
+
stream.write(f"{now()} {message}\n")
|
|
101
|
+
except OSError:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def max_sims(self):
|
|
106
|
+
text = str(os.environ.get("MOBSTER_MAX_SIMS") or "").strip()
|
|
107
|
+
if not text:
|
|
108
|
+
return DEFAULT_MAX_SIMS
|
|
109
|
+
try:
|
|
110
|
+
value = int(text)
|
|
111
|
+
except ValueError:
|
|
112
|
+
value = 0
|
|
113
|
+
if value < 1:
|
|
114
|
+
raise SimError("environment", f"MOBSTER_MAX_SIMS is {text!r}; it must be a whole number of 1 or more.",
|
|
115
|
+
f"Unset it to allow {DEFAULT_MAX_SIMS} simulators per device type and runtime.")
|
|
116
|
+
return value
|
|
117
|
+
|
|
118
|
+
def tools(self):
|
|
119
|
+
"""Xcode's tools, read once per manager. SimError("environment") without a usable Xcode."""
|
|
120
|
+
if self._tools is None:
|
|
121
|
+
self._tools = xcode.find_tools(self.run)
|
|
122
|
+
return self._tools
|
|
123
|
+
|
|
124
|
+
def simctl(self, *args, timeout=60, env=None):
|
|
125
|
+
result = self.run([XCRUN, "simctl", *map(str, args)], timeout=timeout, env=env or self.tools().env)
|
|
126
|
+
if not result.ok:
|
|
127
|
+
self._log(f"simctl {' '.join(map(str, args[:3]))}: exit {result.code}: {result.last_line()}")
|
|
128
|
+
return result
|
|
129
|
+
|
|
130
|
+
def _json(self, *args):
|
|
131
|
+
import json
|
|
132
|
+
result = self.simctl("list", *args, "-j", timeout=60)
|
|
133
|
+
try:
|
|
134
|
+
if not result.ok:
|
|
135
|
+
raise ValueError
|
|
136
|
+
return json.loads(result.out)
|
|
137
|
+
except ValueError:
|
|
138
|
+
raise SimError("environment", f"simctl couldn't list simulators: {result.last_line() or 'no output'}",
|
|
139
|
+
"Open Xcode once so it finishes installing its components, then run "
|
|
140
|
+
"`mobster sim doctor`.") from None
|
|
141
|
+
|
|
142
|
+
def _catalog(self):
|
|
143
|
+
data = self._json()
|
|
144
|
+
return simctl.parse_runtimes(data), simctl.parse_device_types(data), simctl.parse_devices(data)
|
|
145
|
+
|
|
146
|
+
def _devices(self):
|
|
147
|
+
return simctl.parse_devices(self._json("devices"))
|
|
148
|
+
|
|
149
|
+
def _choose(self, device=None, runtime=None, catalog=None):
|
|
150
|
+
runtimes, types, devices = catalog or self._catalog()
|
|
151
|
+
chosen_runtime = simctl.choose_runtime(runtimes, runtime)
|
|
152
|
+
return simctl.choose_device_type(types, chosen_runtime, device), chosen_runtime, devices
|
|
153
|
+
|
|
154
|
+
def _try_lease(self, wda_url):
|
|
155
|
+
"""The device lease for this WDA address, or None while another process holds it."""
|
|
156
|
+
from ..journal import JournalError, LeaseHeld
|
|
157
|
+
try:
|
|
158
|
+
return self.lease(wda_url)
|
|
159
|
+
except LeaseHeld:
|
|
160
|
+
return None
|
|
161
|
+
except JournalError as exc:
|
|
162
|
+
raise SimError("simulator", f"Mobster couldn't take the simulator's lock: {exc}",
|
|
163
|
+
"Check that ~/Library/Application Support/app.mobster.desktop/device-leases belongs "
|
|
164
|
+
"to you and is private.") from None
|
|
165
|
+
|
|
166
|
+
# -- acquire -------------------------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
def acquire(self, device: Optional[str] = None, runtime: Optional[str] = None, *,
|
|
169
|
+
timeout: float = 600, cancelled=None) -> SimLease:
|
|
170
|
+
def claim(timing):
|
|
171
|
+
device_type, chosen_runtime, entry, state, lease = self._claim(device, runtime, timing)
|
|
172
|
+
if lease is None:
|
|
173
|
+
from ..verify.actions import ActionNotDispatched
|
|
174
|
+
busy = ActionNotDispatched(self._all_busy(device_type, chosen_runtime))
|
|
175
|
+
else:
|
|
176
|
+
busy = None
|
|
177
|
+
return entry, state, lease, busy, f"Waiting for a free simulator: all {self.max_sims} are in use"
|
|
178
|
+
return self._acquire(claim, timeout, "", cancelled=cancelled, claim_may_create=True)
|
|
179
|
+
|
|
180
|
+
def acquire_udid(self, udid: str, *, timeout: float = 600, cancelled=None) -> SimLease:
|
|
181
|
+
def claim(timing):
|
|
182
|
+
with self.registry.edit() as entries:
|
|
183
|
+
entry = self._entry(udid, entries)
|
|
184
|
+
present = {item.udid: item for item in self._devices()}
|
|
185
|
+
if entry["udid"] not in present:
|
|
186
|
+
raise SimError("simulator", f"{entry['name']} no longer exists.", "Run `mobster sim prune`.")
|
|
187
|
+
lease = self._try_lease(wda_url(entry["wda_port"]))
|
|
188
|
+
if lease is not None:
|
|
189
|
+
entry["last_used_at"] = now()
|
|
190
|
+
return dict(entry), present[entry["udid"]].state, lease, None, ""
|
|
191
|
+
name = entry["name"]
|
|
192
|
+
# No lease was acquired and _make_ready has not run; this timeout cannot hide a device effect.
|
|
193
|
+
from ..verify.actions import ActionNotDispatched
|
|
194
|
+
busy = ActionNotDispatched(SimError(
|
|
195
|
+
"busy", f"{name} is running a check.", "Wait for it to finish, or stop it, then try again."))
|
|
196
|
+
return None, None, None, busy, f"Waiting for {name}: it is running a check"
|
|
197
|
+
return self._acquire(claim, timeout, " by UDID", cancelled=cancelled)
|
|
198
|
+
|
|
199
|
+
def _acquire(self, claim, timeout, how, *, cancelled=None, claim_may_create=False):
|
|
200
|
+
from ..verify.actions import ActionNotDispatched
|
|
201
|
+
|
|
202
|
+
def stopped(*, known):
|
|
203
|
+
error = SimError("stopped", "The suite was stopped before this simulator ran.")
|
|
204
|
+
raise ActionNotDispatched(error) if known else error
|
|
205
|
+
|
|
206
|
+
started = self.clock()
|
|
207
|
+
timing = {}
|
|
208
|
+
if cancelled is not None and cancelled():
|
|
209
|
+
stopped(known=True)
|
|
210
|
+
tools = self.tools()
|
|
211
|
+
deadline, waiting = started + max(0.0, float(timeout)), False
|
|
212
|
+
while True:
|
|
213
|
+
if cancelled is not None and cancelled():
|
|
214
|
+
stopped(known=True)
|
|
215
|
+
entry, state, lease, busy, message = claim(timing)
|
|
216
|
+
if lease is not None:
|
|
217
|
+
break
|
|
218
|
+
if self.clock() >= deadline:
|
|
219
|
+
raise busy
|
|
220
|
+
if not waiting:
|
|
221
|
+
self._say(message)
|
|
222
|
+
waiting = True
|
|
223
|
+
self.sleep(BUSY_POLL)
|
|
224
|
+
held = [lease]
|
|
225
|
+
holders.claim(self.data_dir, entry["udid"], self.who())
|
|
226
|
+
try:
|
|
227
|
+
if cancelled is not None and cancelled():
|
|
228
|
+
stopped(known=not claim_may_create)
|
|
229
|
+
target = self._make_ready(entry, state, held, tools, timing)
|
|
230
|
+
self._sweep_alerts(target)
|
|
231
|
+
except BaseException:
|
|
232
|
+
for item in held:
|
|
233
|
+
item.close()
|
|
234
|
+
holders.clear(self.data_dir, entry["udid"])
|
|
235
|
+
raise
|
|
236
|
+
timing["total"] = round(self.clock() - started, 2)
|
|
237
|
+
self._log(f"acquired {target.name} ({target.udid}) on {target.wda_url}{how}: {timing}")
|
|
238
|
+
return SimLease(target, held, timing, on_release=lambda: holders.clear(self.data_dir, target.udid))
|
|
239
|
+
|
|
240
|
+
def _claim(self, device, runtime, timing):
|
|
241
|
+
with self.registry.edit() as entries:
|
|
242
|
+
# simctl's list is read under the lock: one read before it can miss a simulator another process registered
|
|
243
|
+
device_type, runtime, devices = self._choose(device, runtime)
|
|
244
|
+
present = {item.udid: item for item in devices}
|
|
245
|
+
for entry in [entry for entry in entries if entry["udid"] not in present]:
|
|
246
|
+
entries.remove(entry)
|
|
247
|
+
self._log(f"forgot {entry['udid']} ({entry['name']}): simctl no longer lists it")
|
|
248
|
+
pair = [entry for entry in entries
|
|
249
|
+
if entry["device_type"] == device_type.name and entry["runtime"] == runtime.name]
|
|
250
|
+
pair.sort(key=lambda entry: entry.get("last_used_at") or "", reverse=True)
|
|
251
|
+
pair.sort(key=lambda entry: present[entry["udid"]].state != "Booted")
|
|
252
|
+
entry, lease = self._first_free(pair)
|
|
253
|
+
if lease is None and pair and len(pair) < self.max_sims:
|
|
254
|
+
# Every lease was held, and a create (a cold boot, and 1.6 GB of disk) is next. `list()` and
|
|
255
|
+
# doctor take and release each lease for milliseconds to see whether it is in use, while a run
|
|
256
|
+
# holds its lease for the whole check. So look once more first (review round 3).
|
|
257
|
+
self.sleep(PROBE_RETRY)
|
|
258
|
+
entry, lease = self._first_free(pair)
|
|
259
|
+
if lease is not None:
|
|
260
|
+
if present[entry["udid"]].state != "Booted":
|
|
261
|
+
self._memory_room(lease, entries, present, device_type, runtime, mine=entry)
|
|
262
|
+
entry["last_used_at"] = now()
|
|
263
|
+
return device_type, runtime, dict(entry), present[entry["udid"]].state, lease
|
|
264
|
+
if len(pair) >= self.max_sims:
|
|
265
|
+
return device_type, runtime, None, None, None
|
|
266
|
+
self._memory_room(None, entries, present, device_type, runtime)
|
|
267
|
+
entry, lease = self._create(device_type, runtime, devices, entries, timing)
|
|
268
|
+
return device_type, runtime, dict(entry), "Shutdown", lease
|
|
269
|
+
|
|
270
|
+
def _memory_room(self, lease, entries, present, device_type, runtime, mine=None):
|
|
271
|
+
"""Refuse at once, rather than boot one simulator too many: each needs SIM_GB free; the first always boots.
|
|
272
|
+
|
|
273
|
+
A simulator another session has leased but not finished booting counts as running, with its memory set aside:
|
|
274
|
+
until it boots, the free memory doesn't show it, and two sessions booting at once would both see room."""
|
|
275
|
+
booted = [entry for entry in entries if present.get(entry["udid"]) and present[entry["udid"]].state == "Booted"]
|
|
276
|
+
starting = [entry for entry in entries if entry is not mine and entry not in booted
|
|
277
|
+
and present.get(entry["udid"]) and self._in_use(entry)]
|
|
278
|
+
running = len(booted) + len(starting)
|
|
279
|
+
pressure = self.governor.sample()
|
|
280
|
+
if self.governor.memory_room(running, pressure, starting=len(starting)):
|
|
281
|
+
return
|
|
282
|
+
if lease is not None:
|
|
283
|
+
lease.close()
|
|
284
|
+
raise self._all_busy(device_type, runtime, free_gb=pressure.free_gb, running=running)
|
|
285
|
+
|
|
286
|
+
def _all_busy(self, device_type, runtime, free_gb=None, running=None):
|
|
287
|
+
entries = self.registry.read()
|
|
288
|
+
who = [holders.words(record) for entry in entries
|
|
289
|
+
if entry["device_type"] == device_type.name and entry["runtime"] == runtime.name
|
|
290
|
+
for record in [holders.read(self.data_dir, entry["udid"])] if record]
|
|
291
|
+
by = f", held by {'; '.join(who)}" if who else ""
|
|
292
|
+
fix = ("A simulator is free when its session ends or has been idle for 30 minutes. Call again then, or ask "
|
|
293
|
+
"the other agent to stop.")
|
|
294
|
+
if free_gb is not None:
|
|
295
|
+
return SimError("busy", f"Every simulator is busy: only {free_gb:.1f} GB of memory is free and another "
|
|
296
|
+
f"needs {SIM_GB} GB, with {running} in use{by}.", fix)
|
|
297
|
+
return SimError("busy", f"Every simulator is busy: all {self.max_sims} for {device_type.name} with "
|
|
298
|
+
f"{runtime.name} are in use{by}.", fix + " MOBSTER_MAX_SIMS raises the limit.")
|
|
299
|
+
|
|
300
|
+
def _first_free(self, entries):
|
|
301
|
+
"""(entry, lease) for the first of ``entries`` whose lease is free, else (None, None)."""
|
|
302
|
+
for entry in entries:
|
|
303
|
+
lease = self._try_lease(wda_url(entry["wda_port"]))
|
|
304
|
+
if lease is not None:
|
|
305
|
+
return entry, lease
|
|
306
|
+
return None, None
|
|
307
|
+
|
|
308
|
+
def _create(self, device_type, runtime, devices, entries, timing):
|
|
309
|
+
name = simctl.sim_name(device_type.name, runtime.name,
|
|
310
|
+
{item.name for item in devices} | {entry["name"] for entry in entries})
|
|
311
|
+
self._say(f"Creating simulator {name}")
|
|
312
|
+
started = self.clock()
|
|
313
|
+
result = self.simctl("create", name, device_type.identifier, runtime.identifier, timeout=120)
|
|
314
|
+
udid = simctl.parse_created(result.out) if result.ok else None
|
|
315
|
+
if not udid:
|
|
316
|
+
raise SimError("simulator", f"simctl couldn't create {name}: {result.last_line() or 'no UDID'}",
|
|
317
|
+
"Run `mobster sim doctor`, then try again.")
|
|
318
|
+
timing["create"] = round(self.clock() - started, 2)
|
|
319
|
+
try:
|
|
320
|
+
wda_port, mjpeg_port, lease = self._claim_ports(taken_ports(entries))
|
|
321
|
+
except BaseException:
|
|
322
|
+
self.simctl("delete", udid, timeout=60) # just created, never registered: nothing else knows it
|
|
323
|
+
raise
|
|
324
|
+
stamp = now()
|
|
325
|
+
entry = {"udid": udid, "name": name, "device_type": device_type.name, "runtime": runtime.name,
|
|
326
|
+
"wda_port": wda_port, "mjpeg_port": mjpeg_port, "created_at": stamp, "last_used_at": stamp}
|
|
327
|
+
entries.append(entry)
|
|
328
|
+
self._log(f"created {name} ({udid}) on ports {wda_port}/{mjpeg_port}")
|
|
329
|
+
return entry, lease
|
|
330
|
+
|
|
331
|
+
def _make_ready(self, entry, state, held, tools, timing):
|
|
332
|
+
udid, name = entry["udid"], entry["name"]
|
|
333
|
+
folder = apps.device_folder(udid)
|
|
334
|
+
if not folder.is_dir():
|
|
335
|
+
raise SimError("simulator", f"{name}'s folder {folder} is missing, so Mobster can't keep its apps "
|
|
336
|
+
"apart from other simulators'.",
|
|
337
|
+
f"Delete it with `mobster sim delete {udid}`, then try again.")
|
|
338
|
+
build = {}
|
|
339
|
+
builder = None
|
|
340
|
+
needs_build = find_xctestrun(build_dir(self.data_dir, tools.build)) is None
|
|
341
|
+
if state != "Booted" or needs_build:
|
|
342
|
+
try:
|
|
343
|
+
self.governor.wait_for_calm(self._say, f"boot {name}" if state != "Booted" else "build WebDriverAgent")
|
|
344
|
+
except SimError as busy:
|
|
345
|
+
# No simctl boot, build or runner start has been issued yet, so a refusal here is a rejected acquire.
|
|
346
|
+
# A simulator this acquire just created is an effect: that refusal stays unknown, as a stop after the
|
|
347
|
+
# claim does (``stopped(known=not claim_may_create)``).
|
|
348
|
+
if "create" in timing:
|
|
349
|
+
raise
|
|
350
|
+
from ..verify.actions import ActionNotDispatched
|
|
351
|
+
raise ActionNotDispatched(busy) from None
|
|
352
|
+
if needs_build:
|
|
353
|
+
def build_runner():
|
|
354
|
+
began = self.clock()
|
|
355
|
+
try:
|
|
356
|
+
build["plan"] = self.wda.ensure_build(tools)
|
|
357
|
+
build["seconds"] = round(self.clock() - began, 2)
|
|
358
|
+
except BaseException as exc: # handed to the waiting thread below
|
|
359
|
+
build["error"] = exc
|
|
360
|
+
# The build (once per Xcode version) runs while the simulator boots.
|
|
361
|
+
builder = threading.Thread(target=build_runner, name="mobster-wda-build", daemon=True)
|
|
362
|
+
builder.start()
|
|
363
|
+
configurer = []
|
|
364
|
+
|
|
365
|
+
def configure_once():
|
|
366
|
+
if configurer:
|
|
367
|
+
return
|
|
368
|
+
|
|
369
|
+
def configure():
|
|
370
|
+
began = self.clock()
|
|
371
|
+
try:
|
|
372
|
+
self._configure(udid)
|
|
373
|
+
except Exception as exc: # the run can go ahead; the log says what was skipped
|
|
374
|
+
self._log(f"settings for {udid} failed: {type(exc).__name__}: {exc}")
|
|
375
|
+
return
|
|
376
|
+
timing["configure"] = round(self.clock() - began, 2)
|
|
377
|
+
configurer.append(threading.Thread(target=configure, name="mobster-sim-configure", daemon=True))
|
|
378
|
+
configurer[0].start()
|
|
379
|
+
try:
|
|
380
|
+
if state != "Booted":
|
|
381
|
+
started = self.clock()
|
|
382
|
+
self._say(f"Booting {name}")
|
|
383
|
+
with heartbeat(self._say, f"booting {name}"):
|
|
384
|
+
self._boot(udid, name, state)
|
|
385
|
+
timing["boot"] = round(self.clock() - started, 2)
|
|
386
|
+
self._say(f"Simulator: {name} (booted in {timing['boot']:.1f} s)")
|
|
387
|
+
configure_once()
|
|
388
|
+
else:
|
|
389
|
+
self._say(f"Simulator: {name}")
|
|
390
|
+
if builder is not None:
|
|
391
|
+
builder.join()
|
|
392
|
+
if "error" in build:
|
|
393
|
+
raise build["error"]
|
|
394
|
+
xctestrun = build["plan"]
|
|
395
|
+
timing["wda_build"] = build["seconds"]
|
|
396
|
+
else:
|
|
397
|
+
xctestrun = self.wda.ensure_build(tools)
|
|
398
|
+
entry = self._runner(entry, held, xctestrun, tools, timing, configure_once)
|
|
399
|
+
except BaseException:
|
|
400
|
+
if builder is not None: # never leave xcodebuild behind a failed acquire (no-op once it finished)
|
|
401
|
+
self.wda.cancel()
|
|
402
|
+
builder.join(timeout=10)
|
|
403
|
+
raise
|
|
404
|
+
finally:
|
|
405
|
+
for thread in configurer:
|
|
406
|
+
thread.join()
|
|
407
|
+
return SimTarget(udid=udid, name=name, device_type=entry["device_type"], runtime=entry["runtime"],
|
|
408
|
+
wda_url=wda_url(entry["wda_port"]), mjpeg_url=wda_url(entry["mjpeg_port"]),
|
|
409
|
+
xctestrun=str(xctestrun))
|
|
410
|
+
|
|
411
|
+
def _boot(self, udid, name, state):
|
|
412
|
+
if state != "Booting":
|
|
413
|
+
result = self.simctl("boot", udid, timeout=BOOT_TIMEOUT)
|
|
414
|
+
text = result.out + result.err
|
|
415
|
+
if not result.ok and "current state: Booted" not in text:
|
|
416
|
+
if result.timed_out:
|
|
417
|
+
raise self._still_booting(name)
|
|
418
|
+
if "Unable to boot" not in text:
|
|
419
|
+
raise self._boot_failed(udid, name, result)
|
|
420
|
+
self.simctl("shutdown", udid, timeout=60)
|
|
421
|
+
self.sleep(1)
|
|
422
|
+
result = self.simctl("boot", udid, timeout=BOOT_TIMEOUT)
|
|
423
|
+
if not result.ok and "current state: Booted" not in result.out + result.err:
|
|
424
|
+
raise self._still_booting(name) if result.timed_out else self._boot_failed(udid, name, result)
|
|
425
|
+
result = self.simctl("bootstatus", udid, "-b", timeout=BOOT_TIMEOUT)
|
|
426
|
+
if not result.ok:
|
|
427
|
+
if result.timed_out:
|
|
428
|
+
raise self._still_booting(name)
|
|
429
|
+
raise SimError("simulator", f"{name} didn't finish booting: {result.last_line() or 'no answer'}",
|
|
430
|
+
f"Erase it with `mobster sim erase {udid}`, then try again.")
|
|
431
|
+
|
|
432
|
+
@staticmethod
|
|
433
|
+
def _still_booting(name):
|
|
434
|
+
return SimError("simulator", f"{name} is still booting after {BOOT_TIMEOUT} s, which happens when this Mac "
|
|
435
|
+
"is busy.", "Run the command again; the simulator keeps booting.")
|
|
436
|
+
|
|
437
|
+
@staticmethod
|
|
438
|
+
def _boot_failed(udid, name, result):
|
|
439
|
+
return SimError("simulator", f"{name} didn't boot: {result.last_line()}",
|
|
440
|
+
f"Erase it with `mobster sim erase {udid}`, then try again.")
|
|
441
|
+
|
|
442
|
+
def _configure(self, udid):
|
|
443
|
+
calls = [("status_bar", udid, "override", *STATUS_BAR)]
|
|
444
|
+
calls += [("spawn", udid, "defaults", "write", "com.apple.Preferences", key, "-bool", "false")
|
|
445
|
+
for key in KEYBOARD_DEFAULTS]
|
|
446
|
+
calls += [("spawn", udid, "defaults", "write", KEYBOARD_PREFERENCES, key, "-bool", value)
|
|
447
|
+
for key, value in KEYBOARD_INTRO]
|
|
448
|
+
self._simctl_all(calls)
|
|
449
|
+
|
|
450
|
+
# each call takes 0.2-0.5 s, mostly waiting on CoreSimulator, so independent ones run at once
|
|
451
|
+
def _simctl_all(self, calls, timeout=30):
|
|
452
|
+
if len(calls) == 1:
|
|
453
|
+
return [self.simctl(*calls[0], timeout=timeout)]
|
|
454
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
455
|
+
with ThreadPoolExecutor(max_workers=len(calls), thread_name_prefix="mobster-simctl") as pool:
|
|
456
|
+
return list(pool.map(lambda call: self.simctl(*call, timeout=timeout), calls))
|
|
457
|
+
|
|
458
|
+
def _runner(self, entry, held, xctestrun, tools, timing, on_start=None):
|
|
459
|
+
udid, name = entry["udid"], entry["name"]
|
|
460
|
+
port, mjpeg = entry["wda_port"], entry["mjpeg_port"]
|
|
461
|
+
plan = self.wda.plan(udid, port, mjpeg, xctestrun)
|
|
462
|
+
self._log(f"runner plan for {udid} on {port}: {plan}")
|
|
463
|
+
if plan == REUSE:
|
|
464
|
+
return entry
|
|
465
|
+
if on_start is not None:
|
|
466
|
+
on_start()
|
|
467
|
+
if plan == MOVE:
|
|
468
|
+
entry = self._move(entry, held)
|
|
469
|
+
self._say(f"Port {port} is in use by another program, so {name}'s WebDriverAgent moves to "
|
|
470
|
+
f"{entry['wda_port']}")
|
|
471
|
+
port, mjpeg = entry["wda_port"], entry["mjpeg_port"]
|
|
472
|
+
self.wda.stop(udid)
|
|
473
|
+
elif plan == RESTART:
|
|
474
|
+
self.wda.stop(udid)
|
|
475
|
+
self._say(f"Starting WebDriverAgent on {LOOPBACK}:{port}")
|
|
476
|
+
started = self.clock()
|
|
477
|
+
process = self.wda.start(udid, port, mjpeg, xctestrun, tools)
|
|
478
|
+
try:
|
|
479
|
+
self.wda.wait_ready(udid, port, process)
|
|
480
|
+
ok, found = self.wda.check_loopback(port, mjpeg)
|
|
481
|
+
missing = [number for number, items in found.items() if not items]
|
|
482
|
+
exposed = any(item.host != LOOPBACK for items in found.values() for item in items)
|
|
483
|
+
if not ok and missing and not exposed:
|
|
484
|
+
raise SimError("wda", f"WebDriverAgent answered on {port} but nothing was seen listening on "
|
|
485
|
+
f"{', '.join(map(str, missing))} within {MJPEG_WAIT:g} s",
|
|
486
|
+
f"Run the command again; see {self.wda.runner_log(udid)}.")
|
|
487
|
+
if not ok:
|
|
488
|
+
seen = "; ".join(f"{number}: " + (", ".join(f"{item.host}:{item.port}" for item in items) or
|
|
489
|
+
"nothing")
|
|
490
|
+
for number, items in found.items())
|
|
491
|
+
raise SimError("wda", "WebDriverAgent listened beyond this Mac",
|
|
492
|
+
f"lsof showed {seen}. Delete {build_dir(self.data_dir, tools.build)} and try again.")
|
|
493
|
+
except BaseException:
|
|
494
|
+
self.wda.stop(udid)
|
|
495
|
+
raise
|
|
496
|
+
timing["wda_start"] = round(self.clock() - started, 2)
|
|
497
|
+
self._say(f"WebDriverAgent ready on {LOOPBACK}:{port} in {timing['wda_start']:.1f} s")
|
|
498
|
+
return entry
|
|
499
|
+
|
|
500
|
+
def _claim_ports(self, taken):
|
|
501
|
+
taken = set(taken)
|
|
502
|
+
for _ in range(40):
|
|
503
|
+
wda_port, mjpeg_port = free_slot(port_base(), taken, busy=self.wda.listening)
|
|
504
|
+
lease = self._try_lease(wda_url(wda_port))
|
|
505
|
+
if lease is not None:
|
|
506
|
+
return wda_port, mjpeg_port, lease
|
|
507
|
+
taken |= {wda_port, mjpeg_port}
|
|
508
|
+
raise SimError("busy", "Every free WebDriverAgent port is locked by another Mobster process.",
|
|
509
|
+
"Wait for the other runs to finish, then try again.")
|
|
510
|
+
|
|
511
|
+
def _move(self, entry, held):
|
|
512
|
+
"""Give the entry the next free port pair, holding that address's lease too."""
|
|
513
|
+
with self.registry.edit() as entries:
|
|
514
|
+
wda_port, mjpeg_port, lease = self._claim_ports(
|
|
515
|
+
taken_ports(entries) | {entry["wda_port"], entry["mjpeg_port"]})
|
|
516
|
+
held.append(lease)
|
|
517
|
+
for item in entries:
|
|
518
|
+
if item["udid"] == entry["udid"]:
|
|
519
|
+
item["wda_port"], item["mjpeg_port"] = wda_port, mjpeg_port
|
|
520
|
+
return {**entry, "wda_port": wda_port, "mjpeg_port": mjpeg_port}
|
|
521
|
+
|
|
522
|
+
# -- apps ----------------------------------------------------------------------------------------------
|
|
523
|
+
|
|
524
|
+
def app_info(self, app_path: str) -> AppInfo:
|
|
525
|
+
"""The .app's identity from its Info.plist. SimError("install") for a device build or a non-app."""
|
|
526
|
+
info = apps.read_info(app_path)
|
|
527
|
+
bundle = str(info.get("CFBundleIdentifier") or "").strip()
|
|
528
|
+
if not bundle:
|
|
529
|
+
raise SimError("install", f"{app_path} has no CFBundleIdentifier in its Info.plist.",
|
|
530
|
+
"Build the app again.")
|
|
531
|
+
if not apps.built_for_simulator(info):
|
|
532
|
+
raise apps.device_build_error(app_path)
|
|
533
|
+
return AppInfo(bundle_id=bundle, name=apps.display_name(info, bundle), version=apps.version_text(info),
|
|
534
|
+
path=str(Path(app_path).expanduser().resolve()), icon=apps.icon_png(app_path, info),
|
|
535
|
+
executable=_executable(info))
|
|
536
|
+
|
|
537
|
+
def installed_app(self, target: SimTarget, bundle_id: str) -> AppInfo:
|
|
538
|
+
result = self.simctl("get_app_container", target.udid, bundle_id, "app", timeout=30)
|
|
539
|
+
path = result.out.strip() if result.ok else ""
|
|
540
|
+
if not path:
|
|
541
|
+
raise SimError("install", f"{bundle_id} isn't installed on the simulator. Pass --app.",
|
|
542
|
+
"Pass the .app built for the iOS Simulator with --app.")
|
|
543
|
+
info = apps.read_info(path)
|
|
544
|
+
return AppInfo(bundle_id=bundle_id, name=apps.display_name(info, bundle_id),
|
|
545
|
+
version=apps.version_text(info), path=None, icon=apps.icon_png(path, info),
|
|
546
|
+
executable=_executable(info))
|
|
547
|
+
|
|
548
|
+
def install(self, target: SimTarget, app_path: str) -> AppInfo:
|
|
549
|
+
info = self.app_info(app_path)
|
|
550
|
+
# Each scheme as declared and in lowercase, since a link's scheme matches without case.
|
|
551
|
+
schemes = list(dict.fromkeys(form for scheme in apps.url_schemes(apps.read_info(info.path))
|
|
552
|
+
for form in (scheme, scheme.lower())))
|
|
553
|
+
started = self.clock()
|
|
554
|
+
approvals = [("spawn", target.udid, "defaults", "write", SCHEME_APPROVALS, f"{URL_OPENER}-->{scheme}",
|
|
555
|
+
"-string", info.bundle_id) for scheme in schemes]
|
|
556
|
+
result, *approved = self._simctl_all([("install", target.udid, info.path)] + approvals, timeout=300)
|
|
557
|
+
if not result.ok:
|
|
558
|
+
raise SimError("install", f"{info.name} didn't install on {target.name}: "
|
|
559
|
+
f"{result.last_line() or 'simctl failed'}",
|
|
560
|
+
"Build it again for the iOS Simulator, then try again.")
|
|
561
|
+
known = {scheme for scheme, done in zip(schemes, approved) if done.ok and scheme == scheme.lower()}
|
|
562
|
+
self._approved.setdefault(target.udid, set()).update(known)
|
|
563
|
+
self._log(f"installed {info.bundle_id} {info.version} on {target.udid} in {self.clock() - started:.2f} s"
|
|
564
|
+
+ (f"; links open without the prompt: {', '.join(sorted(known))}" if known else ""))
|
|
565
|
+
return info
|
|
566
|
+
|
|
567
|
+
def reset(self, target: SimTarget, bundle_id: str, level: str, app_path: Optional[str] = None) -> None:
|
|
568
|
+
if level not in ("none", "data", "reinstall"):
|
|
569
|
+
raise ValueError(f"reset level must be none, data or reinstall, not {level!r}")
|
|
570
|
+
if level != "none" and not bundle_id.startswith("com.apple."):
|
|
571
|
+
started = self.clock()
|
|
572
|
+
if level == "data":
|
|
573
|
+
self._reset_data(target, bundle_id)
|
|
574
|
+
else:
|
|
575
|
+
self._reinstall(target, bundle_id, app_path)
|
|
576
|
+
self._log(f"reset {bundle_id} ({level}) on {target.udid} in {self.clock() - started:.2f} s")
|
|
577
|
+
# After the app is gone, never alongside: WebDriverAgent reads the app in front, and reading one that
|
|
578
|
+
# is being terminated or uninstalled held a reinstall for 10 s (smoke, 28 Sep).
|
|
579
|
+
self._sweep_alerts(target)
|
|
580
|
+
|
|
581
|
+
def _reset_data(self, target, bundle_id):
|
|
582
|
+
udid = target.udid
|
|
583
|
+
result, grouped, _ = self._simctl_all([("get_app_container", udid, bundle_id, "data"),
|
|
584
|
+
("get_app_container", udid, bundle_id, "groups"),
|
|
585
|
+
("terminate", udid, bundle_id)])
|
|
586
|
+
data = result.out.strip() if result.ok else ""
|
|
587
|
+
if data in ("", "(null)"):
|
|
588
|
+
raise SimError("install", f"{bundle_id} isn't installed on the simulator. Pass --app.",
|
|
589
|
+
"Pass the .app built for the iOS Simulator with --app.")
|
|
590
|
+
groups = simctl.parse_groups(grouped.out if grouped.ok else "")
|
|
591
|
+
containers = [data] + [path for _, path in groups]
|
|
592
|
+
for container in containers: # every path is checked before anything changes
|
|
593
|
+
apps.guard_container(container, udid)
|
|
594
|
+
# cfprefsd caches preferences: deleting the plist alone would leave them alive until it restarts.
|
|
595
|
+
calls = [("spawn", udid, "defaults", "delete", domain)
|
|
596
|
+
for domain in [bundle_id] + [group for group, _ in groups if group]]
|
|
597
|
+
calls += [("privacy", udid, "reset", "all", bundle_id), ("keychain", udid, "reset")]
|
|
598
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
599
|
+
with ThreadPoolExecutor(max_workers=1, thread_name_prefix="mobster-reset") as pool:
|
|
600
|
+
pending = pool.submit(self._simctl_all, calls)
|
|
601
|
+
for container in containers:
|
|
602
|
+
apps.empty_container(container, udid)
|
|
603
|
+
pending.result()
|
|
604
|
+
|
|
605
|
+
def _reinstall(self, target, bundle_id, app_path):
|
|
606
|
+
if not app_path:
|
|
607
|
+
raise SimError("install", f"Reinstalling {bundle_id} needs its .app.",
|
|
608
|
+
"Pass the .app built for the iOS Simulator with --app.")
|
|
609
|
+
info = self.app_info(app_path) # a device build fails here, before the installed app is removed
|
|
610
|
+
for bundle in dict.fromkeys((bundle_id, info.bundle_id)):
|
|
611
|
+
self.simctl("uninstall", target.udid, bundle, timeout=120)
|
|
612
|
+
self.install(target, app_path)
|
|
613
|
+
self._privacy_and_keychain(target.udid, info.bundle_id)
|
|
614
|
+
|
|
615
|
+
def _privacy_and_keychain(self, udid, bundle_id):
|
|
616
|
+
# The keychain reset covers the whole simulator, which is fine: the simulator is Mobster's own.
|
|
617
|
+
self._simctl_all([("privacy", udid, "reset", "all", bundle_id), ("keychain", udid, "reset")])
|
|
618
|
+
|
|
619
|
+
def launch(self, target: SimTarget, bundle_id: str, args=(), env: Optional[dict] = None) -> None:
|
|
620
|
+
tools = self.tools()
|
|
621
|
+
child = dict(tools.env)
|
|
622
|
+
for key, value in (env or {}).items():
|
|
623
|
+
child[f"SIMCTL_CHILD_{key}"] = str(value)
|
|
624
|
+
result = self.run([XCRUN, "simctl", "launch", "--terminate-running-process", target.udid, bundle_id,
|
|
625
|
+
*map(str, args)], timeout=60, env=child)
|
|
626
|
+
if not result.ok:
|
|
627
|
+
raise SimError("launch", f"{bundle_id} didn't launch: {result.last_line() or 'simctl failed'}",
|
|
628
|
+
"Check that the app is installed and starts in the iOS Simulator.")
|
|
629
|
+
|
|
630
|
+
def terminate(self, target: SimTarget, bundle_id: str) -> None:
|
|
631
|
+
self.simctl("terminate", target.udid, bundle_id, timeout=30) # "not running" is fine
|
|
632
|
+
|
|
633
|
+
def open_url(self, target: SimTarget, url: str) -> None:
|
|
634
|
+
scheme = urlsplit(url).scheme.lower()
|
|
635
|
+
approved = self._link_approved(target.udid, scheme)
|
|
636
|
+
result = self.simctl("openurl", target.udid, url, timeout=60)
|
|
637
|
+
if not result.ok:
|
|
638
|
+
raise SimError("launch", f"{url} didn't open: {result.last_line() or 'simctl failed'}",
|
|
639
|
+
"Check that the app declares the URL scheme in its Info.plist.")
|
|
640
|
+
if approved:
|
|
641
|
+
return
|
|
642
|
+
wda_url, started = target.wda_url, self.clock()
|
|
643
|
+
answered, text = self.wda.read_alert(wda_url)
|
|
644
|
+
# A read the runner didn't answer (it can take 10 s on a loaded Mac) doesn't end the wait early.
|
|
645
|
+
while text is None and (self.clock() < started + OPEN_PROMPT_WAIT or not answered) \
|
|
646
|
+
and self.clock() < started + OPEN_PROMPT_LIMIT:
|
|
647
|
+
self.sleep(ALERT_POLL)
|
|
648
|
+
answered, text = self.wda.read_alert(wda_url)
|
|
649
|
+
if text is None:
|
|
650
|
+
return
|
|
651
|
+
if not text.startswith(OPEN_PROMPT):
|
|
652
|
+
self._log(f"open_url {url}: left the alert {text!r} for the run")
|
|
653
|
+
return
|
|
654
|
+
for _ in range(2):
|
|
655
|
+
self.wda.alert_action(wda_url, "accept", OPEN_BUTTON)
|
|
656
|
+
if self._alert_after(wda_url, text) != text:
|
|
657
|
+
self._log(f"open_url {url}: pressed {OPEN_BUTTON} on {text!r}")
|
|
658
|
+
if scheme:
|
|
659
|
+
self._approved.setdefault(target.udid, set()).add(scheme) # iOS remembers it too
|
|
660
|
+
return
|
|
661
|
+
raise SimError("launch", f"{url} didn't open: iOS's “{text}” prompt stayed up after Mobster pressed "
|
|
662
|
+
f"{OPEN_BUTTON}.", "Run the check again.")
|
|
663
|
+
|
|
664
|
+
def _link_approved(self, udid, scheme):
|
|
665
|
+
"""True when iOS already has Open as the answer for links of this scheme opened by `simctl openurl`."""
|
|
666
|
+
if not scheme:
|
|
667
|
+
return False
|
|
668
|
+
if scheme in self._approved.get(udid, ()):
|
|
669
|
+
return True
|
|
670
|
+
return bool(apps.scheme_approvals(udid).get(f"{URL_OPENER}-->{scheme}"))
|
|
671
|
+
|
|
672
|
+
def _alert_after(self, wda_url, text):
|
|
673
|
+
end = self.clock() + ALERT_GONE_WAIT
|
|
674
|
+
while True:
|
|
675
|
+
seen = self.wda.alert_text(wda_url)
|
|
676
|
+
if seen != text or self.clock() >= end:
|
|
677
|
+
return seen
|
|
678
|
+
self.sleep(ALERT_POLL)
|
|
679
|
+
|
|
680
|
+
def _sweep_alerts(self, target):
|
|
681
|
+
wda_url = target.wda_url
|
|
682
|
+
text = self.wda.alert_text(wda_url)
|
|
683
|
+
for _ in range(SWEEP_ROUNDS):
|
|
684
|
+
if text is None:
|
|
685
|
+
return
|
|
686
|
+
self._say(f"Dismissing an alert left on {target.name}: “{text}”")
|
|
687
|
+
self.wda.alert_action(wda_url, "dismiss")
|
|
688
|
+
text = self._alert_after(wda_url, text)
|
|
689
|
+
if text is not None:
|
|
690
|
+
raise SimError("simulator", f"An alert stays on {target.name}'s screen: “{text}”.",
|
|
691
|
+
f"Run the command again; if it stays, erase the simulator with "
|
|
692
|
+
f"`mobster sim erase {target.udid}`.")
|
|
693
|
+
|
|
694
|
+
def screenshot(self, target: SimTarget, path, *, max_width: Optional[int] = None, quality: int = 75) -> Path:
|
|
695
|
+
"""A JPEG of the screen through `simctl io` (never through WDA), scaled down to ``max_width``; a .png path
|
|
696
|
+
gets the whole screen as a PNG."""
|
|
697
|
+
from PIL import Image
|
|
698
|
+
path = Path(path)
|
|
699
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
700
|
+
raw = path.with_name(f".{path.stem}.{os.getpid()}.{threading.get_ident()}.png")
|
|
701
|
+
try:
|
|
702
|
+
result = simctl.screenshot(target.udid, raw, timeout=30, env=self.tools().env, runner=self.run)
|
|
703
|
+
if not result.ok:
|
|
704
|
+
self._log(f"simctl io {target.udid} screenshot: exit {result.code}: {result.last_line()}")
|
|
705
|
+
raise SimError("simulator", f"The screenshot failed: {result.last_line() or 'no image'}",
|
|
706
|
+
"Check that the simulator is booted (`mobster sim list`).")
|
|
707
|
+
with Image.open(raw) as image:
|
|
708
|
+
image = image.convert("RGB")
|
|
709
|
+
if path.suffix.lower() == ".png": # a baseline compares pixels: lossless, at full size
|
|
710
|
+
image.save(path, "PNG")
|
|
711
|
+
return path
|
|
712
|
+
if max_width and image.width > max_width:
|
|
713
|
+
height = max(1, round(image.height * max_width / image.width))
|
|
714
|
+
image = image.resize((int(max_width), height), Image.LANCZOS)
|
|
715
|
+
image.save(path, "JPEG", quality=int(quality), optimize=True)
|
|
716
|
+
finally:
|
|
717
|
+
try:
|
|
718
|
+
raw.unlink()
|
|
719
|
+
except OSError:
|
|
720
|
+
pass
|
|
721
|
+
return path
|
|
722
|
+
|
|
723
|
+
def evidence_tools(self, target: SimTarget):
|
|
724
|
+
from ..verify.evidence import SimulatorTools
|
|
725
|
+
return SimulatorTools(target.udid)
|
|
726
|
+
|
|
727
|
+
def restarter(self, target: SimTarget) -> Callable:
|
|
728
|
+
return SimulatorWDA(target.udid, _port(target.wda_url), _port(target.mjpeg_url), target.xctestrun,
|
|
729
|
+
log_path=str(self.wda.runner_log(target.udid)), tools=self.tools())
|
|
730
|
+
|
|
731
|
+
# -- housekeeping --------------------------------------------------------------------------------------
|
|
732
|
+
|
|
733
|
+
def biometric(self, udid: str, action: str, *, biometric_type: str = "face-id") -> dict:
|
|
734
|
+
"""Inspect enrollment or send one biometric test event to an idle, booted Mobster simulator."""
|
|
735
|
+
from .biometrics import perform
|
|
736
|
+
return perform(self, udid, action, biometric_type)
|
|
737
|
+
|
|
738
|
+
def _in_use(self, entry):
|
|
739
|
+
lease = self._try_lease(wda_url(entry["wda_port"]))
|
|
740
|
+
if lease is None:
|
|
741
|
+
return True
|
|
742
|
+
lease.close()
|
|
743
|
+
return False
|
|
744
|
+
|
|
745
|
+
def release_mine(self) -> list:
|
|
746
|
+
"""Ask every simulator held by an agent started from the same app as this process to let go.
|
|
747
|
+
|
|
748
|
+
An agent's `mobster mcp` and the shell it runs this in share a parent chain. Returns the UDIDs asked."""
|
|
749
|
+
mine = set(holders.ancestors())
|
|
750
|
+
asked = []
|
|
751
|
+
for entry in self.registry.read():
|
|
752
|
+
record = holders.read(self.data_dir, entry["udid"])
|
|
753
|
+
if record is not None and record.get("ppid") in mine:
|
|
754
|
+
holders.request_release(self.data_dir, entry["udid"])
|
|
755
|
+
asked.append(entry["udid"])
|
|
756
|
+
return asked
|
|
757
|
+
|
|
758
|
+
def list(self) -> list:
|
|
759
|
+
entries = self.registry.read()
|
|
760
|
+
if not entries:
|
|
761
|
+
return []
|
|
762
|
+
present = {item.udid: item for item in self._devices()}
|
|
763
|
+
rows = []
|
|
764
|
+
for entry in entries:
|
|
765
|
+
device = present.get(entry["udid"])
|
|
766
|
+
state = device.state if device else "Missing"
|
|
767
|
+
url = wda_url(entry["wda_port"])
|
|
768
|
+
in_use = self._in_use(entry) if device else False
|
|
769
|
+
record = holders.read(self.data_dir, entry["udid"]) if in_use else None
|
|
770
|
+
holder = {key: record.get(key) for key in ("client", "session", "pid", "since")} if record else None
|
|
771
|
+
rows.append({"udid": entry["udid"], "name": entry["name"], "device_type": entry["device_type"],
|
|
772
|
+
"runtime": entry["runtime"], "state": state,
|
|
773
|
+
"wda": "ready" if state == "Booted" and self.wda.ready(url) else "stopped",
|
|
774
|
+
"wda_url": url, "mjpeg_url": wda_url(entry["mjpeg_port"]),
|
|
775
|
+
"in_use": in_use, "holder": holder,
|
|
776
|
+
"last_used_at": entry.get("last_used_at")})
|
|
777
|
+
return rows
|
|
778
|
+
|
|
779
|
+
def _entry(self, udid, entries=None):
|
|
780
|
+
entries = self.registry.read() if entries is None else entries
|
|
781
|
+
entry = next((entry for entry in entries if entry["udid"].upper() == str(udid).strip().upper()), None)
|
|
782
|
+
if entry is None:
|
|
783
|
+
raise SimError("simulator", f"{udid} isn't one of Mobster's simulators, so Mobster won't change it.",
|
|
784
|
+
"See Mobster's simulators with `mobster sim list`.")
|
|
785
|
+
return entry
|
|
786
|
+
|
|
787
|
+
def _stop_one(self, entry, state):
|
|
788
|
+
"""Stop the runner and shut the simulator down (its lease held by the caller)."""
|
|
789
|
+
udid = entry["udid"]
|
|
790
|
+
self.wda.stop(udid)
|
|
791
|
+
if state not in ("Shutdown", "Missing"):
|
|
792
|
+
result = self.simctl("shutdown", udid, timeout=120)
|
|
793
|
+
if not result.ok and "current state: Shutdown" not in result.out + result.err:
|
|
794
|
+
raise SimError("simulator", f"{entry['name']} didn't shut down: {result.last_line()}",
|
|
795
|
+
"Try again in a moment.")
|
|
796
|
+
|
|
797
|
+
def _with_lease(self, entry, action, strict):
|
|
798
|
+
lease = self._try_lease(wda_url(entry["wda_port"]))
|
|
799
|
+
if lease is None:
|
|
800
|
+
if strict:
|
|
801
|
+
raise SimError("busy", f"{entry['name']} is running a check.",
|
|
802
|
+
"Wait for it to finish, or stop it, then try again.")
|
|
803
|
+
self._say(f"Skipped {entry['name']}: it is running a check")
|
|
804
|
+
return False
|
|
805
|
+
try:
|
|
806
|
+
action()
|
|
807
|
+
finally:
|
|
808
|
+
lease.close()
|
|
809
|
+
return True
|
|
810
|
+
|
|
811
|
+
def shutdown(self, udid: Optional[str] = None) -> None:
|
|
812
|
+
"""Stop WebDriverAgent and shut down one Mobster simulator, or every one that isn't running a check."""
|
|
813
|
+
self.tools()
|
|
814
|
+
entries = [self._entry(udid)] if udid else self.registry.read()
|
|
815
|
+
states = {item.udid: item.state for item in self._devices()} if entries else {}
|
|
816
|
+
for entry in entries:
|
|
817
|
+
state = states.get(entry["udid"], "Missing")
|
|
818
|
+
if self._with_lease(entry, lambda entry=entry, state=state: self._stop_one(entry, state), bool(udid)):
|
|
819
|
+
if state not in ("Shutdown", "Missing"):
|
|
820
|
+
self._say(f"Shut down {entry['name']}")
|
|
821
|
+
|
|
822
|
+
def _guard_owned(self, entry, devices):
|
|
823
|
+
current = next((item for item in devices if item.udid == entry["udid"]), None)
|
|
824
|
+
names = [entry["name"]] + ([current.name] if current else [])
|
|
825
|
+
if not all(simctl.is_mobster_name(name) for name in names):
|
|
826
|
+
shown = current.name if current else entry["name"]
|
|
827
|
+
raise SimError("simulator", f"{entry['udid']} is named “{shown}”, not a Mobster simulator name, so "
|
|
828
|
+
"Mobster won't change it.",
|
|
829
|
+
"Rename it back, or delete it yourself in Xcode.")
|
|
830
|
+
return current
|
|
831
|
+
|
|
832
|
+
def erase(self, udid: str) -> None:
|
|
833
|
+
"""Shut down and erase one Mobster simulator; the next acquire boots it fresh."""
|
|
834
|
+
self.tools()
|
|
835
|
+
entry = self._entry(udid)
|
|
836
|
+
current = self._guard_owned(entry, self._devices())
|
|
837
|
+
if current is None:
|
|
838
|
+
raise SimError("simulator", f"{entry['name']} no longer exists.", "Run `mobster sim prune`.")
|
|
839
|
+
|
|
840
|
+
def erase_it():
|
|
841
|
+
self._stop_one(entry, current.state)
|
|
842
|
+
result = self.simctl("erase", entry["udid"], timeout=120)
|
|
843
|
+
self._approved.pop(entry["udid"], None) # erasing forgets iOS's answers too
|
|
844
|
+
if not result.ok:
|
|
845
|
+
raise SimError("simulator", f"{entry['name']} wasn't erased: {result.last_line()}",
|
|
846
|
+
"Try again in a moment.")
|
|
847
|
+
self._say(f"Erased {entry['name']}")
|
|
848
|
+
self._with_lease(entry, erase_it, True)
|
|
849
|
+
|
|
850
|
+
def delete(self, udid: Optional[str] = None) -> list:
|
|
851
|
+
self.tools()
|
|
852
|
+
entries = self.registry.read()
|
|
853
|
+
devices = self._devices() if entries else []
|
|
854
|
+
chosen = [self._entry(udid, entries)] if udid else list(entries)
|
|
855
|
+
deleted = []
|
|
856
|
+
for entry in chosen:
|
|
857
|
+
try:
|
|
858
|
+
current = self._guard_owned(entry, devices)
|
|
859
|
+
except SimError:
|
|
860
|
+
if udid:
|
|
861
|
+
raise
|
|
862
|
+
self._say(f"Skipped {entry['udid']}: its name no longer starts with “Mobster · ”")
|
|
863
|
+
continue
|
|
864
|
+
|
|
865
|
+
def delete_it(entry=entry, current=current):
|
|
866
|
+
if current is not None:
|
|
867
|
+
self._stop_one(entry, current.state)
|
|
868
|
+
result = self.simctl("delete", entry["udid"], timeout=120)
|
|
869
|
+
if not result.ok:
|
|
870
|
+
raise SimError("simulator", f"{entry['name']} wasn't deleted: {result.last_line()}",
|
|
871
|
+
"Try again in a moment.")
|
|
872
|
+
deleted.append(entry["udid"])
|
|
873
|
+
self._say(f"Deleted {entry['name']}")
|
|
874
|
+
self._forget(entry["udid"])
|
|
875
|
+
self._with_lease(entry, delete_it, bool(udid))
|
|
876
|
+
return deleted
|
|
877
|
+
|
|
878
|
+
def _forget(self, udid):
|
|
879
|
+
self._approved.pop(udid, None)
|
|
880
|
+
with self.registry.edit() as entries:
|
|
881
|
+
entries[:] = [entry for entry in entries if entry["udid"] != udid]
|
|
882
|
+
try:
|
|
883
|
+
self.wda.runner_log(udid).unlink()
|
|
884
|
+
except OSError:
|
|
885
|
+
pass
|
|
886
|
+
|
|
887
|
+
def prune(self) -> dict:
|
|
888
|
+
import shutil
|
|
889
|
+
tools = self.tools()
|
|
890
|
+
removed = {"simulators": [], "entries": [], "wda_builds": []}
|
|
891
|
+
with self.registry.edit() as entries:
|
|
892
|
+
present = {item.udid for item in self._devices()} # under the lock, as in _claim
|
|
893
|
+
for entry in [entry for entry in entries if entry["udid"] not in present]:
|
|
894
|
+
entries.remove(entry)
|
|
895
|
+
removed["entries"].append(entry["udid"])
|
|
896
|
+
cutoff = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=PRUNE_DAYS)
|
|
897
|
+
for entry in self.registry.read():
|
|
898
|
+
used = _parse_time(entry.get("last_used_at")) or _parse_time(entry.get("created_at"))
|
|
899
|
+
if used is not None and used < cutoff:
|
|
900
|
+
try:
|
|
901
|
+
removed["simulators"] += self.delete(entry["udid"])
|
|
902
|
+
except SimError as exc:
|
|
903
|
+
self._say(f"Kept {entry['name']}: {exc}")
|
|
904
|
+
installed = xcode.installed_builds() | {tools.build}
|
|
905
|
+
running = self.wda.ps()
|
|
906
|
+
from .wda import COMMIT8, PATCH_DIGEST, parse_build_dir
|
|
907
|
+
folder = self.data_dir / "wda"
|
|
908
|
+
for path in sorted(folder.glob("build-*")) if folder.is_dir() else ():
|
|
909
|
+
parts = parse_build_dir(path.name)
|
|
910
|
+
stale = parts is None or parts[0] not in installed or parts[1:] != (COMMIT8, PATCH_DIGEST)
|
|
911
|
+
if stale and str(path) not in running:
|
|
912
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
913
|
+
removed["wda_builds"].append(str(path))
|
|
914
|
+
for path in sorted(folder.glob("src-*")) if folder.is_dir() else ():
|
|
915
|
+
if path.name != f"src-{COMMIT8}":
|
|
916
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
917
|
+
removed["wda_builds"].append(str(path))
|
|
918
|
+
self._log(f"pruned {removed}")
|
|
919
|
+
return removed
|
|
920
|
+
|
|
921
|
+
def status(self) -> dict:
|
|
922
|
+
try:
|
|
923
|
+
tools = self.tools()
|
|
924
|
+
xcode = {"version": tools.version, "build": tools.build,
|
|
925
|
+
"path": str(Path(tools.xcodebuild).parents[4])}
|
|
926
|
+
except SimError as exc:
|
|
927
|
+
tools, xcode = None, {"error": str(exc)}
|
|
928
|
+
runtime = device_type = None
|
|
929
|
+
simulators = []
|
|
930
|
+
if tools is not None:
|
|
931
|
+
try:
|
|
932
|
+
chosen_type, chosen_runtime, _ = self._choose()
|
|
933
|
+
runtime, device_type = chosen_runtime.name, chosen_type.name
|
|
934
|
+
except SimError as exc:
|
|
935
|
+
runtime = {"error": str(exc)}
|
|
936
|
+
try:
|
|
937
|
+
simulators = self.list()
|
|
938
|
+
except SimError as exc:
|
|
939
|
+
simulators = [{"error": str(exc)}]
|
|
940
|
+
plan = find_xctestrun(build_dir(self.data_dir, tools.build)) if tools else None
|
|
941
|
+
return {"xcode": xcode, "runtime": runtime, "device_type": device_type,
|
|
942
|
+
"wda_build": {"cached": plan is not None, "path": str(plan) if plan else None},
|
|
943
|
+
"simulators": simulators, "data_dir": str(self.data_dir), "max_sims": self.max_sims}
|
|
944
|
+
|
|
945
|
+
def doctor(self, fix: bool = False) -> list:
|
|
946
|
+
from .doctor import run_checks
|
|
947
|
+
return run_checks(self, fix=fix)
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def _executable(info):
|
|
951
|
+
name = info.get("CFBundleExecutable") if isinstance(info, dict) else None
|
|
952
|
+
return name.strip() if isinstance(name, str) and name.strip() else None
|