mobster-cli 0.4.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mobile_agent/__init__.py +4 -0
- mobile_agent/__main__.py +598 -0
- mobile_agent/accounts/__init__.py +1 -0
- mobile_agent/accounts/context.py +80 -0
- mobile_agent/accounts/mcp.py +55 -0
- mobile_agent/accounts/pacing.py +318 -0
- mobile_agent/accounts/register.py +78 -0
- mobile_agent/accounts/routes.py +212 -0
- mobile_agent/accounts/routing.py +69 -0
- mobile_agent/accounts/screen.py +273 -0
- mobile_agent/accounts/stats.py +54 -0
- mobile_agent/accounts/store.py +394 -0
- mobile_agent/agent_hooks.py +123 -0
- mobile_agent/ai_plans/__init__.py +7 -0
- mobile_agent/ai_plans/agents.py +96 -0
- mobile_agent/ai_plans/claude_code.py +208 -0
- mobile_agent/ai_plans/cli.py +400 -0
- mobile_agent/ai_plans/codex.py +258 -0
- mobile_agent/ai_plans/compat_chat.py +307 -0
- mobile_agent/ai_plans/device_login.py +164 -0
- mobile_agent/ai_plans/draft_client.py +68 -0
- mobile_agent/ai_plans/gemini.py +137 -0
- mobile_agent/ai_plans/instructions.py +65 -0
- mobile_agent/ai_plans/mcp_http.py +160 -0
- mobile_agent/ai_plans/phone_tools.py +892 -0
- mobile_agent/ai_plans/providers.py +235 -0
- mobile_agent/ai_plans/routes.py +160 -0
- mobile_agent/ai_plans/runner.py +400 -0
- mobile_agent/ai_plans/vault.py +114 -0
- mobile_agent/alerts.py +337 -0
- mobile_agent/android/__init__.py +18 -0
- mobile_agent/android/adb.py +268 -0
- mobile_agent/android/conditions.py +128 -0
- mobile_agent/android/doctor.py +54 -0
- mobile_agent/android/driver.py +137 -0
- mobile_agent/android/emulator.py +216 -0
- mobile_agent/android/logs.py +85 -0
- mobile_agent/android/manager.py +225 -0
- mobile_agent/android/records.py +31 -0
- mobile_agent/android/tree.py +96 -0
- mobile_agent/answer_shapes.py +230 -0
- mobile_agent/api_devices.py +333 -0
- mobile_agent/api_errors.py +17 -0
- mobile_agent/api_get.py +249 -0
- mobile_agent/api_http.py +467 -0
- mobile_agent/api_post.py +453 -0
- mobile_agent/api_routes.py +173 -0
- mobile_agent/app_api/__init__.py +4 -0
- mobile_agent/app_api/cli.py +151 -0
- mobile_agent/app_api/endpoints.py +193 -0
- mobile_agent/app_api/mcp.py +54 -0
- mobile_agent/app_api/openapi.py +146 -0
- mobile_agent/app_api/reading.py +172 -0
- mobile_agent/app_api/register.py +10 -0
- mobile_agent/app_api/screen.py +216 -0
- mobile_agent/app_api/service.py +382 -0
- mobile_agent/app_api/sessions.py +404 -0
- mobile_agent/app_api/tokens.py +167 -0
- mobile_agent/app_choice.py +131 -0
- mobile_agent/app_icons.py +183 -0
- mobile_agent/app_killer.py +69 -0
- mobile_agent/artifacts.py +606 -0
- mobile_agent/attach/__init__.py +13 -0
- mobile_agent/attach/cli.py +46 -0
- mobile_agent/attach/flutter.py +28 -0
- mobile_agent/attach/score.py +119 -0
- mobile_agent/attach/web.py +13 -0
- mobile_agent/attachments/__init__.py +1 -0
- mobile_agent/attachments/agent.py +418 -0
- mobile_agent/attachments/extract.py +217 -0
- mobile_agent/attachments/mcp.py +104 -0
- mobile_agent/attachments/register.py +73 -0
- mobile_agent/attachments/routes.py +326 -0
- mobile_agent/attachments/service.py +170 -0
- mobile_agent/attachments/sniff.py +152 -0
- mobile_agent/attachments/store.py +441 -0
- mobile_agent/audit_verify.py +436 -0
- mobile_agent/bench/README.md +410 -0
- mobile_agent/bench/__init__.py +0 -0
- mobile_agent/bench/__main__.py +394 -0
- mobile_agent/bench/ab.py +168 -0
- mobile_agent/bench/actuation.py +368 -0
- mobile_agent/bench/agents/__init__.py +9 -0
- mobile_agent/bench/agents/base.py +105 -0
- mobile_agent/bench/agents/plan_clients.py +245 -0
- mobile_agent/bench/agents/smart.py +533 -0
- mobile_agent/bench/checks.py +358 -0
- mobile_agent/bench/decide_gate.py +214 -0
- mobile_agent/bench/diagnose.py +181 -0
- mobile_agent/bench/evals/__init__.py +14 -0
- mobile_agent/bench/evals/build.py +565 -0
- mobile_agent/bench/evals/checkpoint.py +214 -0
- mobile_agent/bench/evals/irt.py +219 -0
- mobile_agent/bench/evals/l0.py +647 -0
- mobile_agent/bench/evals/l1.py +866 -0
- mobile_agent/bench/evals/models.py +124 -0
- mobile_agent/bench/evals/pairwise.py +189 -0
- mobile_agent/bench/evals/proxy.py +90 -0
- mobile_agent/bench/evals/sets.py +149 -0
- mobile_agent/bench/evals/traces.py +446 -0
- mobile_agent/bench/fixtures.py +176 -0
- mobile_agent/bench/guard_replay.py +129 -0
- mobile_agent/bench/h2-manifest.json +20 -0
- mobile_agent/bench/h3-manifest.json +20 -0
- mobile_agent/bench/heldout-manifest.json +32 -0
- mobile_agent/bench/heldout.py +375 -0
- mobile_agent/bench/iosworld.py +901 -0
- mobile_agent/bench/iosworld_stats.py +510 -0
- mobile_agent/bench/llm_units.py +116 -0
- mobile_agent/bench/manifest.json +93 -0
- mobile_agent/bench/metrics.py +66 -0
- mobile_agent/bench/phone.py +44 -0
- mobile_agent/bench/replay.py +172 -0
- mobile_agent/bench/report.py +207 -0
- mobile_agent/bench/runner.py +456 -0
- mobile_agent/bench/safety.py +310 -0
- mobile_agent/bench/stream_timing.py +213 -0
- mobile_agent/bench/suite.py +444 -0
- mobile_agent/bench/sweep.py +176 -0
- mobile_agent/bench/tape.py +798 -0
- mobile_agent/bench/tape_replay.py +88 -0
- mobile_agent/bench/trace_evals.py +128 -0
- mobile_agent/bench/trace_view.py +81 -0
- mobile_agent/bench/truth/iphone15pro.json +138 -0
- mobile_agent/bench/truth.py +340 -0
- mobile_agent/bench/tuning.py +543 -0
- mobile_agent/bench/tuning_analysis.py +656 -0
- mobile_agent/bench/vague-heldout-manifest.json +12 -0
- mobile_agent/bench/vague_rubric.py +135 -0
- mobile_agent/bench/vague_run.py +243 -0
- mobile_agent/brand.py +454 -0
- mobile_agent/brand_fonts.py +756 -0
- mobile_agent/build_identity.py +21 -0
- mobile_agent/cable_bt.py +623 -0
- mobile_agent/cable_bt_runtime.py +128 -0
- mobile_agent/catalog.py +69 -0
- mobile_agent/chart_svg.py +415 -0
- mobile_agent/chat_here.py +189 -0
- mobile_agent/cli_constants.py +5 -0
- mobile_agent/cli_parser.py +611 -0
- mobile_agent/cli_run.py +115 -0
- mobile_agent/compiled_read/__init__.py +21 -0
- mobile_agent/compiled_read/delta.py +89 -0
- mobile_agent/compiled_read/extractors.py +202 -0
- mobile_agent/compiled_read/routes.py +56 -0
- mobile_agent/completion.py +267 -0
- mobile_agent/compose.py +48 -0
- mobile_agent/config.py +114 -0
- mobile_agent/connectors/__init__.py +1 -0
- mobile_agent/connectors/api_session.py +76 -0
- mobile_agent/connectors/callback_page.py +146 -0
- mobile_agent/connectors/client.py +582 -0
- mobile_agent/connectors/github_app.py +55 -0
- mobile_agent/connectors/graph_api.py +396 -0
- mobile_agent/connectors/imports.py +170 -0
- mobile_agent/connectors/issue.py +66 -0
- mobile_agent/connectors/keystore.py +85 -0
- mobile_agent/connectors/marks.py +222 -0
- mobile_agent/connectors/oauth.py +476 -0
- mobile_agent/connectors/picker.py +228 -0
- mobile_agent/connectors/presets.py +591 -0
- mobile_agent/connectors/register.py +48 -0
- mobile_agent/connectors/routes.py +153 -0
- mobile_agent/connectors/service.py +966 -0
- mobile_agent/connectors/shared.py +136 -0
- mobile_agent/connectors/skill.py +446 -0
- mobile_agent/connectors/slack_api.py +269 -0
- mobile_agent/connectors/steps.py +92 -0
- mobile_agent/connectors/store.py +144 -0
- mobile_agent/connectors/vault.py +72 -0
- mobile_agent/console.py +178 -0
- mobile_agent/content_queue/__init__.py +1 -0
- mobile_agent/content_queue/client.py +263 -0
- mobile_agent/content_queue/importing.py +153 -0
- mobile_agent/content_queue/media_check.py +224 -0
- mobile_agent/content_queue/prompt.py +64 -0
- mobile_agent/content_queue/register.py +91 -0
- mobile_agent/content_queue/routes.py +370 -0
- mobile_agent/content_queue/runner.py +475 -0
- mobile_agent/content_queue/store.py +521 -0
- mobile_agent/contract.py +996 -0
- mobile_agent/contract_asks.py +248 -0
- mobile_agent/contract_prompt.py +45 -0
- mobile_agent/contract_screen.py +981 -0
- mobile_agent/contract_view.py +84 -0
- mobile_agent/corpus/__init__.py +26 -0
- mobile_agent/corpus/cli.py +63 -0
- mobile_agent/corpus/consent.py +55 -0
- mobile_agent/corpus/listener.py +76 -0
- mobile_agent/corpus/register.py +9 -0
- mobile_agent/corpus/skeleton.py +72 -0
- mobile_agent/corpus/store.py +143 -0
- mobile_agent/costs.py +161 -0
- mobile_agent/decider_scope.py +40 -0
- mobile_agent/decision_provider.py +189 -0
- mobile_agent/deeplinks.py +352 -0
- mobile_agent/device_info.py +116 -0
- mobile_agent/device_manager.py +978 -0
- mobile_agent/device_status.py +195 -0
- mobile_agent/device_targets.py +217 -0
- mobile_agent/devices.py +676 -0
- mobile_agent/devkit/__init__.py +64 -0
- mobile_agent/devkit/account_cli.py +90 -0
- mobile_agent/devkit/asc.py +562 -0
- mobile_agent/devkit/asc_cli.py +214 -0
- mobile_agent/devkit/ci.py +737 -0
- mobile_agent/devkit/ci_cli.py +196 -0
- mobile_agent/devkit/ci_hosts.py +194 -0
- mobile_agent/devkit/es256.py +197 -0
- mobile_agent/devkit/expo.py +440 -0
- mobile_agent/devkit/expo_cli.py +405 -0
- mobile_agent/devkit/expo_dev.py +203 -0
- mobile_agent/devkit/expo_routes.py +90 -0
- mobile_agent/devkit/fixtures_cli.py +133 -0
- mobile_agent/devkit/fixtures_routes.py +82 -0
- mobile_agent/devkit/links.py +375 -0
- mobile_agent/devkit/links_cli.py +123 -0
- mobile_agent/devkit/links_routes.py +83 -0
- mobile_agent/devkit/nightly.py +152 -0
- mobile_agent/devkit/shots.py +203 -0
- mobile_agent/devkit/testflight.py +155 -0
- mobile_agent/devkit/testflight_cli.py +127 -0
- mobile_agent/devkit/testflight_routes.py +111 -0
- mobile_agent/devkit/xcode.py +236 -0
- mobile_agent/devkit/xcode_cli.py +101 -0
- mobile_agent/devtools.py +419 -0
- mobile_agent/doctor.py +509 -0
- mobile_agent/doctor_fix.py +467 -0
- mobile_agent/downloads.py +55 -0
- mobile_agent/drivers.py +793 -0
- mobile_agent/dry_run.py +102 -0
- mobile_agent/effect_ledger.py +83 -0
- mobile_agent/engine_health.py +378 -0
- mobile_agent/engine_hooks.py +96 -0
- mobile_agent/engines.py +408 -0
- mobile_agent/env_file.py +63 -0
- mobile_agent/env_secrets.py +149 -0
- mobile_agent/errors.py +15 -0
- mobile_agent/evals/__init__.py +3 -0
- mobile_agent/evals/app_survey.py +149 -0
- mobile_agent/evals/development/__init__.py +1 -0
- mobile_agent/evals/development/score.py +276 -0
- mobile_agent/evals/latency_report.py +312 -0
- mobile_agent/evals/offline_report.py +374 -0
- mobile_agent/evals/oracles.py +298 -0
- mobile_agent/evals/redteam/__init__.py +15 -0
- mobile_agent/evals/redteam/__main__.py +72 -0
- mobile_agent/evals/redteam/cases.py +730 -0
- mobile_agent/evals/redteam/harness.py +604 -0
- mobile_agent/evals/redteam/live.py +139 -0
- mobile_agent/evals/redteam/plain.py +85 -0
- mobile_agent/evals/vision/__init__.py +0 -0
- mobile_agent/evals/vision/images/dog_001.jpg +0 -0
- mobile_agent/evals/vision/images/dog_002.jpg +0 -0
- mobile_agent/evals/vision/images/dog_003.jpg +0 -0
- mobile_agent/evals/vision/images/dog_004.jpg +0 -0
- mobile_agent/evals/vision/images/dog_005.jpg +0 -0
- mobile_agent/evals/vision/images/dog_006.jpg +0 -0
- mobile_agent/evals/vision/images/dog_007.jpg +0 -0
- mobile_agent/evals/vision/images/dog_010.jpg +0 -0
- mobile_agent/evals/vision/images/dog_011.jpg +0 -0
- mobile_agent/evals/vision/images/dog_013.jpg +0 -0
- mobile_agent/evals/vision/images/dog_014.jpg +0 -0
- mobile_agent/evals/vision/images/dog_015.jpg +0 -0
- mobile_agent/evals/vision/images/dog_016.jpg +0 -0
- mobile_agent/evals/vision/images/dog_017.jpg +0 -0
- mobile_agent/evals/vision/images/dog_019.jpg +0 -0
- mobile_agent/evals/vision/images/dog_020.jpg +0 -0
- mobile_agent/evals/vision/images/dog_021.jpg +0 -0
- mobile_agent/evals/vision/images/dog_022.jpg +0 -0
- mobile_agent/evals/vision/images/dog_023.jpg +0 -0
- mobile_agent/evals/vision/images/dog_024.jpg +0 -0
- mobile_agent/evals/vision/images/dog_026.jpg +0 -0
- mobile_agent/evals/vision/images/dog_027.jpg +0 -0
- mobile_agent/evals/vision/images/dog_034.jpg +0 -0
- mobile_agent/evals/vision/images/dog_035.jpg +0 -0
- mobile_agent/evals/vision/images/dog_038.jpg +0 -0
- mobile_agent/evals/vision/images/dog_042.jpg +0 -0
- mobile_agent/evals/vision/images/dog_047.jpg +0 -0
- mobile_agent/evals/vision/images/dog_052.jpg +0 -0
- mobile_agent/evals/vision/images/dog_058.jpg +0 -0
- mobile_agent/evals/vision/images/dog_062.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_000.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_010.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_011.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_015.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_016.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_017.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_019.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_021.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_025.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_026.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_027.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_030.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_032.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_033.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_034.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_035.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_036.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_037.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_039.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_040.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_041.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_042.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_043.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_044.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_046.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_047.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_048.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_049.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_051.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_053.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_054.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_055.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_058.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_002.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_003.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_004.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_007.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_009.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_010.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_011.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_012.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_013.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_014.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_016.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_017.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_023.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_027.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_029.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_032.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_033.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_036.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_041.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_043.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_047.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_049.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_051.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_053.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_056.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_057.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_058.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_060.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_061.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_062.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_063.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_064.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_065.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_066.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_067.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_068.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_069.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_070.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_071.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_000.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_001.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_002.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_004.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_005.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_008.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_009.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_010.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_012.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_013.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_015.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_016.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_017.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_018.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_021.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_022.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_023.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_024.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_030.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_031.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_032.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_033.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_034.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_036.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_038.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_039.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_040.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_041.jpg +0 -0
- mobile_agent/evals/vision/manifest.json +2355 -0
- mobile_agent/eventbus.py +110 -0
- mobile_agent/explain.py +34 -0
- mobile_agent/failure_line.py +197 -0
- mobile_agent/fanout.py +663 -0
- mobile_agent/feedback/__init__.py +2 -0
- mobile_agent/feedback/bundle.py +204 -0
- mobile_agent/feedback/outbox.py +196 -0
- mobile_agent/feedback/register.py +9 -0
- mobile_agent/feedback/routes.py +177 -0
- mobile_agent/fleet.py +841 -0
- mobile_agent/fleet_health.py +729 -0
- mobile_agent/fleet_phone.py +186 -0
- mobile_agent/frontier/__init__.py +79 -0
- mobile_agent/frontier/act.py +380 -0
- mobile_agent/frontier/actions.py +70 -0
- mobile_agent/frontier/agent.py +256 -0
- mobile_agent/frontier/approvals.py +366 -0
- mobile_agent/frontier/hooks.py +199 -0
- mobile_agent/frontier/interrupts.py +125 -0
- mobile_agent/frontier/observe.py +331 -0
- mobile_agent/frontier/phases.py +381 -0
- mobile_agent/frontier/pipeline.py +61 -0
- mobile_agent/frontier/prompt.py +384 -0
- mobile_agent/frontier/protocol.py +91 -0
- mobile_agent/frontier/safety.py +467 -0
- mobile_agent/frontier/saved.py +375 -0
- mobile_agent/frontier/screen.py +419 -0
- mobile_agent/frontier/skills.py +147 -0
- mobile_agent/frontier/stages/__init__.py +1 -0
- mobile_agent/frontier/stages/after.py +192 -0
- mobile_agent/frontier/stages/approve.py +138 -0
- mobile_agent/frontier/stages/controls.py +115 -0
- mobile_agent/frontier/stages/ends.py +102 -0
- mobile_agent/frontier/stages/gate.py +92 -0
- mobile_agent/frontier/stages/guards.py +132 -0
- mobile_agent/frontier/stages/points.py +155 -0
- mobile_agent/frontier/stages/send.py +177 -0
- mobile_agent/frontier/turn.py +151 -0
- mobile_agent/full_disk_access.py +83 -0
- mobile_agent/gemini.py +69 -0
- mobile_agent/genuine.py +120 -0
- mobile_agent/graph/__init__.py +4 -0
- mobile_agent/graph/catalog.py +789 -0
- mobile_agent/graph/chain.py +306 -0
- mobile_agent/graph/dispatch.py +98 -0
- mobile_agent/graph/draft.py +373 -0
- mobile_agent/graph/edit.py +76 -0
- mobile_agent/graph/estimate.py +189 -0
- mobile_agent/graph/expr.py +562 -0
- mobile_agent/graph/functions.py +271 -0
- mobile_agent/graph/infer.py +144 -0
- mobile_agent/graph/nodes.py +936 -0
- mobile_agent/graph/phone_steps.py +178 -0
- mobile_agent/graph/rerun.py +327 -0
- mobile_agent/graph/run.py +899 -0
- mobile_agent/graph/schema.py +842 -0
- mobile_agent/graph/state.py +161 -0
- mobile_agent/graph/values.py +198 -0
- mobile_agent/harness/__init__.py +2 -0
- mobile_agent/harness/checkpoints.py +468 -0
- mobile_agent/harness/control.py +101 -0
- mobile_agent/harness/prewarm.py +213 -0
- mobile_agent/harness/register.py +43 -0
- mobile_agent/harness/routes.py +114 -0
- mobile_agent/harness/settings.py +66 -0
- mobile_agent/harness/tools/__init__.py +67 -0
- mobile_agent/harness/tools/ask_user.py +102 -0
- mobile_agent/harness/tools/watch.py +120 -0
- mobile_agent/harness_api.py +815 -0
- mobile_agent/hedge.py +203 -0
- mobile_agent/help_topics.py +305 -0
- mobile_agent/hints.py +248 -0
- mobile_agent/history.py +69 -0
- mobile_agent/home.py +214 -0
- mobile_agent/http_pool.py +175 -0
- mobile_agent/image_policy.py +139 -0
- mobile_agent/inbox.py +148 -0
- mobile_agent/installed_apps.py +344 -0
- mobile_agent/integrations/__init__.py +1 -0
- mobile_agent/integrations/cli.py +472 -0
- mobile_agent/integrations/client_files.py +65 -0
- mobile_agent/integrations/clients.py +998 -0
- mobile_agent/integrations/doctor.py +185 -0
- mobile_agent/integrations/jsonc.py +383 -0
- mobile_agent/integrations/routes.py +135 -0
- mobile_agent/integrations/rules.py +112 -0
- mobile_agent/integrations/server_options.py +46 -0
- mobile_agent/integrations/skill.py +152 -0
- mobile_agent/integrations/skill_files.py +10 -0
- mobile_agent/integrations/textedit.py +309 -0
- mobile_agent/intents/__init__.py +17 -0
- mobile_agent/intents/atlas.py +216 -0
- mobile_agent/intents/callback.py +44 -0
- mobile_agent/intents/cli.py +55 -0
- mobile_agent/intents/harvest.py +91 -0
- mobile_agent/intents/plan.py +93 -0
- mobile_agent/ios/__init__.py +1 -0
- mobile_agent/ios/guide.py +16 -0
- mobile_agent/ios/kb.py +212 -0
- mobile_agent/ios/kb_data.py +1588 -0
- mobile_agent/ios/mcp.py +44 -0
- mobile_agent/ios/register.py +8 -0
- mobile_agent/ios/system.py +369 -0
- mobile_agent/journal.py +531 -0
- mobile_agent/keychain_acl.py +374 -0
- mobile_agent/keys.py +395 -0
- mobile_agent/latency_overlap.py +36 -0
- mobile_agent/latency_trace.py +170 -0
- mobile_agent/learned/__init__.py +15 -0
- mobile_agent/learned/app_path.py +160 -0
- mobile_agent/learned/code_read.py +38 -0
- mobile_agent/learned/parts.py +546 -0
- mobile_agent/learned/programs.py +691 -0
- mobile_agent/learned/register.py +171 -0
- mobile_agent/learned/routes.py +61 -0
- mobile_agent/learned/taught.py +329 -0
- mobile_agent/learned/teach.py +538 -0
- mobile_agent/learned/workflows.py +647 -0
- mobile_agent/learned/zero.py +41 -0
- mobile_agent/links.py +37 -0
- mobile_agent/listen.py +607 -0
- mobile_agent/lock_spike.py +278 -0
- mobile_agent/lockscreen.py +526 -0
- mobile_agent/login.py +354 -0
- mobile_agent/longform/__init__.py +15 -0
- mobile_agent/longform/ads.py +191 -0
- mobile_agent/longform/blocks.py +117 -0
- mobile_agent/longform/budget.py +265 -0
- mobile_agent/longform/context.py +285 -0
- mobile_agent/longform/costs.py +93 -0
- mobile_agent/longform/digest.py +145 -0
- mobile_agent/longform/extract.py +508 -0
- mobile_agent/longform/feed.py +505 -0
- mobile_agent/longform/followup.py +168 -0
- mobile_agent/longform/frames.py +82 -0
- mobile_agent/longform/identity.py +87 -0
- mobile_agent/longform/inbox.py +221 -0
- mobile_agent/longform/items.py +425 -0
- mobile_agent/longform/models.py +115 -0
- mobile_agent/longform/notebook.py +329 -0
- mobile_agent/longform/plan.py +614 -0
- mobile_agent/longform/recall.py +195 -0
- mobile_agent/longform/receipts.py +81 -0
- mobile_agent/longform/register.py +124 -0
- mobile_agent/longform/report.py +343 -0
- mobile_agent/longform/routes.py +102 -0
- mobile_agent/longform/session.py +644 -0
- mobile_agent/longform/store.py +146 -0
- mobile_agent/longform/synthesis.py +347 -0
- mobile_agent/mac_tools.py +223 -0
- mobile_agent/manual_control.py +143 -0
- mobile_agent/maps.py +149 -0
- mobile_agent/marks.py +92 -0
- mobile_agent/mcp_server/__init__.py +3 -0
- mobile_agent/mcp_server/__main__.py +29 -0
- mobile_agent/mcp_server/action_journal.py +24 -0
- mobile_agent/mcp_server/action_tools.py +638 -0
- mobile_agent/mcp_server/approvals.py +219 -0
- mobile_agent/mcp_server/catalog.py +92 -0
- mobile_agent/mcp_server/cli.py +194 -0
- mobile_agent/mcp_server/codes.py +147 -0
- mobile_agent/mcp_server/device_tools.py +306 -0
- mobile_agent/mcp_server/direct.py +272 -0
- mobile_agent/mcp_server/expo_tools.py +105 -0
- mobile_agent/mcp_server/phone_tools.py +510 -0
- mobile_agent/mcp_server/protocol.py +364 -0
- mobile_agent/mcp_server/registry.py +55 -0
- mobile_agent/mcp_server/run_tools.py +531 -0
- mobile_agent/mcp_server/schemas.py +425 -0
- mobile_agent/mcp_server/screen_tools.py +168 -0
- mobile_agent/mcp_server/session.py +469 -0
- mobile_agent/mcp_server/support.py +145 -0
- mobile_agent/mcp_server/tools.py +493 -0
- mobile_agent/memory/__init__.py +9 -0
- mobile_agent/memory/cli.py +207 -0
- mobile_agent/memory/extract.py +191 -0
- mobile_agent/memory/listener.py +100 -0
- mobile_agent/memory/model.py +144 -0
- mobile_agent/memory/project.py +96 -0
- mobile_agent/memory/provider.py +127 -0
- mobile_agent/memory/register.py +21 -0
- mobile_agent/memory/retrieval.py +172 -0
- mobile_agent/memory/routes.py +216 -0
- mobile_agent/memory/runs.py +124 -0
- mobile_agent/memory/store.py +806 -0
- mobile_agent/memory/writer.py +278 -0
- mobile_agent/mirroring.py +585 -0
- mobile_agent/mirroring_runtime.py +221 -0
- mobile_agent/model_clients.py +604 -0
- mobile_agent/narrate.py +813 -0
- mobile_agent/native_helpers.py +84 -0
- mobile_agent/netxray/__init__.py +1 -0
- mobile_agent/netxray/analyze.py +322 -0
- mobile_agent/netxray/attribution.py +62 -0
- mobile_agent/netxray/capture.py +88 -0
- mobile_agent/netxray/cli.py +142 -0
- mobile_agent/netxray/job.py +219 -0
- mobile_agent/netxray/mobster-pcap.c +560 -0
- mobile_agent/netxray/packets.py +410 -0
- mobile_agent/netxray/pcapng.py +223 -0
- mobile_agent/netxray/register.py +93 -0
- mobile_agent/netxray/report.py +214 -0
- mobile_agent/netxray/store.py +48 -0
- mobile_agent/netxray/summary.py +157 -0
- mobile_agent/netxray/target.py +67 -0
- mobile_agent/netxray/tool.py +66 -0
- mobile_agent/netxray/trackers.py +194 -0
- mobile_agent/notify_triggers/__init__.py +1 -0
- mobile_agent/notify_triggers/rules.py +39 -0
- mobile_agent/notify_triggers/sources.py +315 -0
- mobile_agent/notify_triggers/watch.py +135 -0
- mobile_agent/offers.py +86 -0
- mobile_agent/orchestrate.py +215 -0
- mobile_agent/outlets/__init__.py +1 -0
- mobile_agent/outlets/connect.py +249 -0
- mobile_agent/outlets/folder.py +180 -0
- mobile_agent/outlets/hooks.py +453 -0
- mobile_agent/outlets/inbound.py +472 -0
- mobile_agent/outlets/inbound_reply.py +57 -0
- mobile_agent/outlets/inbound_routes.py +81 -0
- mobile_agent/outlets/kinds.py +308 -0
- mobile_agent/outlets/mail.py +110 -0
- mobile_agent/outlets/net.py +86 -0
- mobile_agent/outlets/notion.py +244 -0
- mobile_agent/outlets/register.py +56 -0
- mobile_agent/outlets/routes.py +135 -0
- mobile_agent/outlets/service.py +580 -0
- mobile_agent/outlets/sheets.py +169 -0
- mobile_agent/outlets/slack.py +177 -0
- mobile_agent/outlets/store.py +194 -0
- mobile_agent/outlets/vault.py +80 -0
- mobile_agent/outlets/webhook.py +316 -0
- mobile_agent/output_contract.py +141 -0
- mobile_agent/owner_presence.py +211 -0
- mobile_agent/ownership.py +219 -0
- mobile_agent/packs/__init__.py +1 -0
- mobile_agent/packs/calendar.py +243 -0
- mobile_agent/packs/context.py +68 -0
- mobile_agent/packs/core.py +757 -0
- mobile_agent/packs/front_door.py +316 -0
- mobile_agent/packs/heal.py +214 -0
- mobile_agent/packs/health.py +122 -0
- mobile_agent/packs/insights.py +76 -0
- mobile_agent/packs/instagram.py +640 -0
- mobile_agent/packs/linkedin.py +106 -0
- mobile_agent/packs/maps.py +196 -0
- mobile_agent/packs/mcp.py +149 -0
- mobile_agent/packs/messages.py +270 -0
- mobile_agent/packs/notes.py +226 -0
- mobile_agent/packs/photos.py +166 -0
- mobile_agent/packs/pins.py +47 -0
- mobile_agent/packs/reddit.py +110 -0
- mobile_agent/packs/register.py +276 -0
- mobile_agent/packs/rows.py +108 -0
- mobile_agent/packs/safari.py +143 -0
- mobile_agent/packs/settings.py +253 -0
- mobile_agent/packs/social.py +300 -0
- mobile_agent/packs/threads.py +97 -0
- mobile_agent/packs/tiktok.py +455 -0
- mobile_agent/packs/tools.py +290 -0
- mobile_agent/packs/x.py +127 -0
- mobile_agent/packs/youtube.py +199 -0
- mobile_agent/parity.py +270 -0
- mobile_agent/paths.py +99 -0
- mobile_agent/perception.py +470 -0
- mobile_agent/permissions/__init__.py +32 -0
- mobile_agent/permissions/consequence.py +535 -0
- mobile_agent/permissions/exceptions.py +99 -0
- mobile_agent/permissions/gate.py +316 -0
- mobile_agent/permissions/outside.py +316 -0
- mobile_agent/permissions/policy.py +288 -0
- mobile_agent/permissions/project_policy.py +411 -0
- mobile_agent/permissions/register.py +148 -0
- mobile_agent/permissions/rules.py +291 -0
- mobile_agent/phone_io/__init__.py +146 -0
- mobile_agent/phone_io/cli.py +427 -0
- mobile_agent/phone_io/clipboard.py +78 -0
- mobile_agent/phone_io/controls.py +78 -0
- mobile_agent/phone_io/files.py +289 -0
- mobile_agent/phone_io/handoff.py +123 -0
- mobile_agent/phone_io/icloud.py +118 -0
- mobile_agent/phone_io/install.py +188 -0
- mobile_agent/phone_io/network_capture.py +188 -0
- mobile_agent/phone_io/network_cli.py +138 -0
- mobile_agent/phone_io/network_profiles.py +138 -0
- mobile_agent/phone_io/photos.py +512 -0
- mobile_agent/phone_io/recording.py +115 -0
- mobile_agent/phone_io/secrets.py +146 -0
- mobile_agent/phone_io/transfer.py +895 -0
- mobile_agent/phone_io/tunnel.py +129 -0
- mobile_agent/phone_ready.py +91 -0
- mobile_agent/pinning.py +158 -0
- mobile_agent/plan_gate.py +807 -0
- mobile_agent/plugins/__init__.py +3 -0
- mobile_agent/plugins/install.py +163 -0
- mobile_agent/plugins/manifest.py +242 -0
- mobile_agent/plugins/register.py +15 -0
- mobile_agent/plugins/routes.py +82 -0
- mobile_agent/plugins/service.py +322 -0
- mobile_agent/plugins/skills.py +76 -0
- mobile_agent/plugins/store.py +79 -0
- mobile_agent/pool.py +285 -0
- mobile_agent/power_watch.py +275 -0
- mobile_agent/premium_model.py +22 -0
- mobile_agent/project_env.py +291 -0
- mobile_agent/project_env_cli.py +74 -0
- mobile_agent/project_env_routes.py +55 -0
- mobile_agent/projects/__init__.py +27 -0
- mobile_agent/projects/ci.py +332 -0
- mobile_agent/projects/clone.py +339 -0
- mobile_agent/projects/drafts.py +158 -0
- mobile_agent/projects/folder.py +436 -0
- mobile_agent/projects/github.py +131 -0
- mobile_agent/projects/register.py +31 -0
- mobile_agent/projects/revisions.py +253 -0
- mobile_agent/projects/routes.py +234 -0
- mobile_agent/projects/runs.py +297 -0
- mobile_agent/projects/service.py +695 -0
- mobile_agent/projects/setup.py +84 -0
- mobile_agent/projects/store.py +379 -0
- mobile_agent/projects/sync.py +380 -0
- mobile_agent/projects/workflow_files.py +270 -0
- mobile_agent/replay_core/__init__.py +26 -0
- mobile_agent/replay_core/checks.py +594 -0
- mobile_agent/replay_core/engine.py +596 -0
- mobile_agent/replay_core/heal.py +364 -0
- mobile_agent/replay_core/path.py +240 -0
- mobile_agent/replay_core/recorder.py +206 -0
- mobile_agent/replay_core/store.py +171 -0
- mobile_agent/result_blocks.py +941 -0
- mobile_agent/run.py +642 -0
- mobile_agent/run_delivery.py +123 -0
- mobile_agent/run_executor.py +200 -0
- mobile_agent/run_export.py +284 -0
- mobile_agent/run_gif.py +891 -0
- mobile_agent/runner_hints.py +86 -0
- mobile_agent/runner_renewal.py +205 -0
- mobile_agent/runtime_create.py +549 -0
- mobile_agent/runtime_driver.py +458 -0
- mobile_agent/runtime_limits.py +319 -0
- mobile_agent/runtime_queue.py +394 -0
- mobile_agent/runtime_runs.py +383 -0
- mobile_agent/runtime_status.py +181 -0
- mobile_agent/runtime_teach.py +295 -0
- mobile_agent/runtime_work.py +599 -0
- mobile_agent/screen_text.py +101 -0
- mobile_agent/secret_filter.py +113 -0
- mobile_agent/serve.py +160 -0
- mobile_agent/server.py +363 -0
- mobile_agent/service_support.py +42 -0
- mobile_agent/settle/__init__.py +1 -0
- mobile_agent/settle/predict.py +102 -0
- mobile_agent/settle/replay.py +92 -0
- mobile_agent/settle/store.py +93 -0
- mobile_agent/settle/stream.py +43 -0
- mobile_agent/setup_cli.py +225 -0
- mobile_agent/setup_errors.py +87 -0
- mobile_agent/setup_service.py +527 -0
- mobile_agent/share.py +104 -0
- mobile_agent/signing.py +184 -0
- mobile_agent/sim/__init__.py +6 -0
- mobile_agent/sim/__main__.py +26 -0
- mobile_agent/sim/api.py +952 -0
- mobile_agent/sim/apps.py +172 -0
- mobile_agent/sim/biometrics.py +84 -0
- mobile_agent/sim/cli.py +271 -0
- mobile_agent/sim/doctor.py +293 -0
- mobile_agent/sim/governor.py +90 -0
- mobile_agent/sim/holders.py +88 -0
- mobile_agent/sim/models.py +62 -0
- mobile_agent/sim/registry.py +161 -0
- mobile_agent/sim/simctl.py +269 -0
- mobile_agent/sim/wda.py +618 -0
- mobile_agent/sim/xcode.py +74 -0
- mobile_agent/skills/__init__.py +12 -0
- mobile_agent/skills/codes.py +597 -0
- mobile_agent/skills/notifications.py +281 -0
- mobile_agent/skills/open_url.py +127 -0
- mobile_agent/smart_events.py +588 -0
- mobile_agent/smart_result.py +490 -0
- mobile_agent/smart_run.py +413 -0
- mobile_agent/state.py +662 -0
- mobile_agent/states.py +217 -0
- mobile_agent/steering.py +70 -0
- mobile_agent/style.py +178 -0
- mobile_agent/supervised.py +234 -0
- mobile_agent/swift/helper_bt_hid.swift +534 -0
- mobile_agent/swift/helper_doc_text.swift +184 -0
- mobile_agent/swift/helper_draft_reply.swift +170 -0
- mobile_agent/swift/helper_map.swift +110 -0
- mobile_agent/swift/helper_mirroring.swift +1099 -0
- mobile_agent/swift/helper_mjpeg_mp4.swift +103 -0
- mobile_agent/swift/helper_pdf.swift +100 -0
- mobile_agent/swift/helper_phone_audio.swift +993 -0
- mobile_agent/swift/helper_phone_screen.swift +416 -0
- mobile_agent/swift/helper_screen_text.swift +24 -0
- mobile_agent/swift/helper_transfer.swift +246 -0
- mobile_agent/swift/ocr.swift +22 -0
- mobile_agent/task_policy.py +337 -0
- mobile_agent/team_signing/__init__.py +34 -0
- mobile_agent/team_signing/apple_ca.py +37 -0
- mobile_agent/team_signing/bundle.py +136 -0
- mobile_agent/team_signing/cli.py +208 -0
- mobile_agent/team_signing/identity.py +248 -0
- mobile_agent/team_signing/ledger.py +105 -0
- mobile_agent/team_signing/prebuilt.py +61 -0
- mobile_agent/team_signing/resign.py +256 -0
- mobile_agent/team_signing/service.py +382 -0
- mobile_agent/team_signing/tooling.py +243 -0
- mobile_agent/team_signing/writes.py +268 -0
- mobile_agent/terminal_image.py +81 -0
- mobile_agent/testkit/__init__.py +1 -0
- mobile_agent/testkit/a11y_baseline.py +132 -0
- mobile_agent/testkit/api.py +403 -0
- mobile_agent/testkit/capsule.py +400 -0
- mobile_agent/testkit/capsule_cli.py +57 -0
- mobile_agent/testkit/cli.py +948 -0
- mobile_agent/testkit/cli_arguments.py +193 -0
- mobile_agent/testkit/comparison.py +288 -0
- mobile_agent/testkit/comparison_cli.py +41 -0
- mobile_agent/testkit/discover.py +257 -0
- mobile_agent/testkit/editor.py +243 -0
- mobile_agent/testkit/execute.py +515 -0
- mobile_agent/testkit/github_checks.py +248 -0
- mobile_agent/testkit/gitref.py +66 -0
- mobile_agent/testkit/heal_review.py +176 -0
- mobile_agent/testkit/health.py +480 -0
- mobile_agent/testkit/history.py +255 -0
- mobile_agent/testkit/hosted_results.py +321 -0
- mobile_agent/testkit/input_files.py +173 -0
- mobile_agent/testkit/jobs.py +285 -0
- mobile_agent/testkit/junit_verdict.py +33 -0
- mobile_agent/testkit/maestro.py +802 -0
- mobile_agent/testkit/mcp.py +562 -0
- mobile_agent/testkit/mcp_comparison.py +88 -0
- mobile_agent/testkit/minimize.py +471 -0
- mobile_agent/testkit/minimizer_cli.py +48 -0
- mobile_agent/testkit/notify.py +212 -0
- mobile_agent/testkit/phone_video.py +191 -0
- mobile_agent/testkit/pr.py +491 -0
- mobile_agent/testkit/pr_report.py +562 -0
- mobile_agent/testkit/prbuild.py +148 -0
- mobile_agent/testkit/proof_report.py +55 -0
- mobile_agent/testkit/provenance.py +119 -0
- mobile_agent/testkit/prrun.py +244 -0
- mobile_agent/testkit/recent.py +166 -0
- mobile_agent/testkit/recipes.py +146 -0
- mobile_agent/testkit/record.py +204 -0
- mobile_agent/testkit/register.py +288 -0
- mobile_agent/testkit/reports.py +998 -0
- mobile_agent/testkit/repro.py +432 -0
- mobile_agent/testkit/scenario.py +440 -0
- mobile_agent/testkit/scenario_cli.py +37 -0
- mobile_agent/testkit/snapshots_cli.py +60 -0
- mobile_agent/testkit/suite.py +670 -0
- mobile_agent/testkit/targets.py +497 -0
- mobile_agent/testkit/xctest.py +378 -0
- mobile_agent/threads/__init__.py +1 -0
- mobile_agent/threads/chat_once.py +264 -0
- mobile_agent/threads/cli.py +740 -0
- mobile_agent/threads/client.py +326 -0
- mobile_agent/threads/context.py +246 -0
- mobile_agent/threads/mcp.py +739 -0
- mobile_agent/threads/recent.py +34 -0
- mobile_agent/threads/register.py +54 -0
- mobile_agent/threads/routes.py +188 -0
- mobile_agent/threads/service.py +975 -0
- mobile_agent/threads/store.py +361 -0
- mobile_agent/threads/summary.py +117 -0
- mobile_agent/threads/term.py +101 -0
- mobile_agent/threads/workflow_cli.py +104 -0
- mobile_agent/tls.py +36 -0
- mobile_agent/traces.py +271 -0
- mobile_agent/tracks.py +235 -0
- mobile_agent/transport.py +271 -0
- mobile_agent/triggers/__init__.py +1 -0
- mobile_agent/triggers/actions.py +204 -0
- mobile_agent/triggers/model.py +146 -0
- mobile_agent/triggers/register.py +29 -0
- mobile_agent/triggers/routes.py +101 -0
- mobile_agent/triggers/service.py +387 -0
- mobile_agent/triggers/store.py +142 -0
- mobile_agent/tui/__init__.py +89 -0
- mobile_agent/tui/app.py +582 -0
- mobile_agent/tui/chrome.py +321 -0
- mobile_agent/tui/commands.py +375 -0
- mobile_agent/tui/demo_phone.py +369 -0
- mobile_agent/tui/images.py +29 -0
- mobile_agent/tui/keys.py +64 -0
- mobile_agent/tui/prompt.py +195 -0
- mobile_agent/tui/render.py +395 -0
- mobile_agent/tui/screens.py +180 -0
- mobile_agent/tui/session.py +308 -0
- mobile_agent/tui/tasks.py +287 -0
- mobile_agent/tui/theme.py +118 -0
- mobile_agent/tui/widgets.py +272 -0
- mobile_agent/update_check.py +213 -0
- mobile_agent/usage.py +184 -0
- mobile_agent/verify/__init__.py +0 -0
- mobile_agent/verify/__main__.py +41 -0
- mobile_agent/verify/a11y.py +189 -0
- mobile_agent/verify/actions.py +290 -0
- mobile_agent/verify/assertions.py +570 -0
- mobile_agent/verify/audit.py +312 -0
- mobile_agent/verify/business.py +221 -0
- mobile_agent/verify/business_run.py +131 -0
- mobile_agent/verify/business_state.py +168 -0
- mobile_agent/verify/check_schema.py +398 -0
- mobile_agent/verify/checks.py +614 -0
- mobile_agent/verify/cli.py +423 -0
- mobile_agent/verify/conditions.py +483 -0
- mobile_agent/verify/crashes.py +321 -0
- mobile_agent/verify/device_audit.py +246 -0
- mobile_agent/verify/dialogs.py +287 -0
- mobile_agent/verify/evidence.py +705 -0
- mobile_agent/verify/fixtures.py +305 -0
- mobile_agent/verify/flow.py +213 -0
- mobile_agent/verify/l10n.py +141 -0
- mobile_agent/verify/netcerts.py +67 -0
- mobile_agent/verify/netproxy.py +356 -0
- mobile_agent/verify/netrun.py +81 -0
- mobile_agent/verify/netshim.m +28 -0
- mobile_agent/verify/netshim.py +48 -0
- mobile_agent/verify/netstubs.py +132 -0
- mobile_agent/verify/overlay.py +174 -0
- mobile_agent/verify/perf.py +239 -0
- mobile_agent/verify/proof.py +192 -0
- mobile_agent/verify/replay_admission.py +48 -0
- mobile_agent/verify/report.py +952 -0
- mobile_agent/verify/run_cleanup.py +67 -0
- mobile_agent/verify/run_links.py +48 -0
- mobile_agent/verify/run_perf.py +44 -0
- mobile_agent/verify/run_secrets.py +39 -0
- mobile_agent/verify/runner.py +976 -0
- mobile_agent/verify/runs.py +57 -0
- mobile_agent/verify/screen_coverage.py +181 -0
- mobile_agent/verify/smart.py +106 -0
- mobile_agent/verify/snapshot_run.py +114 -0
- mobile_agent/verify/snapshots.py +306 -0
- mobile_agent/verify/sound.py +335 -0
- mobile_agent/verify/source_observation.py +130 -0
- mobile_agent/verify/system_surfaces/__init__.py +7 -0
- mobile_agent/verify/system_surfaces/play.py +106 -0
- mobile_agent/verify/system_surfaces/spec.py +139 -0
- mobile_agent/verify/system_surfaces/stage.py +138 -0
- mobile_agent/verify/system_surfaces/surfaces.py +121 -0
- mobile_agent/verify/tree.py +223 -0
- mobile_agent/verify/verdict.py +377 -0
- mobile_agent/verify/wire.py +392 -0
- mobile_agent/verify/wirestore.py +176 -0
- mobile_agent/waiting/__init__.py +1 -0
- mobile_agent/waiting/contacts.py +73 -0
- mobile_agent/waiting/drafts.py +136 -0
- mobile_agent/waiting/job.py +145 -0
- mobile_agent/waiting/messages.py +174 -0
- mobile_agent/waiting/rank.py +115 -0
- mobile_agent/waiting/register.py +26 -0
- mobile_agent/watch.py +502 -0
- mobile_agent/wda/__init__.py +2 -0
- mobile_agent/wda/frame_clock.py +646 -0
- mobile_agent/wda/frame_source.py +325 -0
- mobile_agent/wda/frame_store.py +115 -0
- mobile_agent/wda/gate.py +163 -0
- mobile_agent/wda/gestures.py +291 -0
- mobile_agent/wda/guards.py +94 -0
- mobile_agent/wda/identity.py +55 -0
- mobile_agent/wda/observe.py +884 -0
- mobile_agent/wda/patches.py +856 -0
- mobile_agent/wda/point_gestures.py +150 -0
- mobile_agent/wda/source.py +32 -0
- mobile_agent/wda/system_dialogs.py +571 -0
- mobile_agent/wda/text.py +694 -0
- mobile_agent/wda/timeouts.py +77 -0
- mobile_agent/wda/video.py +505 -0
- mobile_agent/wda/watchdog.py +243 -0
- mobile_agent/wireless/__init__.py +15 -0
- mobile_agent/wireless/api.py +354 -0
- mobile_agent/wireless/cli.py +287 -0
- mobile_agent/wireless/devicectl.py +229 -0
- mobile_agent/wireless/exposure.py +33 -0
- mobile_agent/wireless/mobster-wifi-pair.c +122 -0
- mobile_agent/wireless/monitor.py +332 -0
- mobile_agent/wireless/network.py +334 -0
- mobile_agent/wireless/onboarding.py +293 -0
- mobile_agent/wireless/pair.py +124 -0
- mobile_agent/wireless/probe.py +322 -0
- mobile_agent/wireless/register.py +8 -0
- mobile_agent/wireless/relay.py +179 -0
- mobile_agent/wireless/result.py +87 -0
- mobile_agent/wireless/store.py +85 -0
- mobile_agent/wireless/switch.py +77 -0
- mobile_agent/wireless/transport.py +118 -0
- mobile_agent/wireless/words.py +81 -0
- mobile_agent/wireless/xctestrun.py +83 -0
- mobile_agent/workflow_tools.py +59 -0
- mobile_agent/workflows/__init__.py +2 -0
- mobile_agent/workflows/brief.py +201 -0
- mobile_agent/workflows/cron.py +110 -0
- mobile_agent/workflows/health.py +257 -0
- mobile_agent/workflows/history.py +126 -0
- mobile_agent/workflows/records.py +442 -0
- mobile_agent/workflows/runs.py +1000 -0
- mobile_agent/workflows/scheduler.py +134 -0
- mobile_agent/workflows/service.py +973 -0
- mobile_agent/workflows/starters.py +285 -0
- mobile_agent/workflows/triggers.py +248 -0
- mobile_agent/xray/__init__.py +1 -0
- mobile_agent/xray/app_cli.py +116 -0
- mobile_agent/xray/cli.py +21 -0
- mobile_agent/xray/crawl.py +754 -0
- mobile_agent/xray/diff.py +171 -0
- mobile_agent/xray/diff_cli.py +43 -0
- mobile_agent/xray/job.py +184 -0
- mobile_agent/xray/paywall.py +188 -0
- mobile_agent/xray/policy.py +305 -0
- mobile_agent/xray/register.py +43 -0
- mobile_agent/xray/report.py +283 -0
- mobile_agent/xray/screens.py +117 -0
- mobster_cli-0.4.3.dist-info/METADATA +93 -0
- mobster_cli-0.4.3.dist-info/RECORD +1009 -0
- mobster_cli-0.4.3.dist-info/WHEEL +5 -0
- mobster_cli-0.4.3.dist-info/entry_points.txt +3 -0
- mobster_cli-0.4.3.dist-info/licenses/LICENSE +21 -0
- mobster_cli-0.4.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""One table of how long each kind of WebDriverAgent request may take, and which requests are actions.
|
|
2
|
+
|
|
3
|
+
``transport.HTTP.request`` always has an absolute deadline, but its own default is 20 s and each call site
|
|
4
|
+
used to pick its own number. This module is the one place that names a bound per class of call, so a read
|
|
5
|
+
of ``/wda/locked`` can never wait as long as a screenshot, and so a test can walk the code and fail a
|
|
6
|
+
WebDriverAgent request that has no bound (tests/test_wda_timeouts.py).
|
|
7
|
+
|
|
8
|
+
``is_action`` is the fail-closed side: a POST that is not known to be a read changes the phone, so the
|
|
9
|
+
watchdog (watchdog.py) never restarts a runner while one is in flight, and nothing resends one.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# Seconds per class of call. Edit here, not at call sites.
|
|
13
|
+
WDA_CALL_TIMEOUTS = {
|
|
14
|
+
# sessionless GET /status, /wda/locked, /wda/activeAppInfo, /wda/screen: milliseconds when healthy
|
|
15
|
+
"probe": 3.0,
|
|
16
|
+
"settings": 5.0, # POST /appium/settings
|
|
17
|
+
"read": 10.0, # /source, /screenshot, /window/size, element reads
|
|
18
|
+
"element": 10.0, # POST /elements, element find
|
|
19
|
+
"action": 10.0, # taps, swipes, keys, button presses, element value and clear, app activate
|
|
20
|
+
"session": 30.0, # POST /session: a cold runner takes seconds to create one
|
|
21
|
+
}
|
|
22
|
+
# No WDA call may ask for more than this, whatever the caller computed.
|
|
23
|
+
MAX_WDA_TIMEOUT = 60.0
|
|
24
|
+
|
|
25
|
+
PROBE_PATHS = ("/status", "/wda/locked", "/wda/activeAppInfo", "/wda/screen", "/wda/device/info",
|
|
26
|
+
"/mobster/photos/status", "/mobster/power")
|
|
27
|
+
# POSTs that read or configure and never change what is on the phone: a find (at the top or under an element) and
|
|
28
|
+
# these prefixes. Every other POST under /element/<id> is an action.
|
|
29
|
+
READ_POST_PREFIXES = ("/appium/settings", "/wda/getPasteboard", "/wda/apps/state")
|
|
30
|
+
FIND_SUFFIXES = ("/element", "/elements")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _path(path):
|
|
34
|
+
path = path.split("?", 1)[0]
|
|
35
|
+
if path.startswith("/session/"):
|
|
36
|
+
path = "/" + path.split("/", 3)[3] if path.count("/") >= 3 else "/"
|
|
37
|
+
return path
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def classify(method, path):
|
|
41
|
+
"""The call class of a WDA request: probe, settings, read, element, action or session."""
|
|
42
|
+
method, path = method.upper(), _path(path)
|
|
43
|
+
if method == "POST" and path == "/session" or path == "/":
|
|
44
|
+
return "session"
|
|
45
|
+
if method == "GET":
|
|
46
|
+
return "probe" if path in PROBE_PATHS else "read"
|
|
47
|
+
if method == "POST" and path.startswith("/appium/settings"):
|
|
48
|
+
return "settings"
|
|
49
|
+
if method == "POST" and path.startswith(("/elements", "/element")) and path.rstrip("/").endswith(("/elements",
|
|
50
|
+
"/element")):
|
|
51
|
+
return "element"
|
|
52
|
+
if is_action(method, path):
|
|
53
|
+
return "action"
|
|
54
|
+
return "read"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def is_action(method, path):
|
|
58
|
+
"""Whether this request can change the phone. Fail closed: any POST or DELETE not known to be a read is one."""
|
|
59
|
+
method, path = method.upper(), _path(path)
|
|
60
|
+
if method == "GET":
|
|
61
|
+
return False
|
|
62
|
+
if method == "POST" and path == "/session":
|
|
63
|
+
return False
|
|
64
|
+
if method == "POST" and (path.rstrip("/").endswith(FIND_SUFFIXES) or path.startswith(READ_POST_PREFIXES)):
|
|
65
|
+
return False
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def timeout_for(method, path):
|
|
70
|
+
return WDA_CALL_TIMEOUTS[classify(method, path)]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def bounded(timeout, method="GET", path="/status"):
|
|
74
|
+
"""``timeout`` clamped to MAX_WDA_TIMEOUT, or the table's value for the call when it is None."""
|
|
75
|
+
if timeout is None:
|
|
76
|
+
return timeout_for(method, path)
|
|
77
|
+
return max(0.001, min(float(timeout), MAX_WDA_TIMEOUT))
|
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
"""Live video from an iPhone through WebDriverAgent's MJPEG stream, relayed to every viewer from one connection."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import http.client
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
8
|
+
|
|
9
|
+
from . import timeouts as wda_timeouts
|
|
10
|
+
|
|
11
|
+
# User-selectable quality. WDA's own default is 10 fps at full resolution, which is
|
|
12
|
+
# what a viewer gets whenever these settings fail to apply; 60 is WDA's ceiling and
|
|
13
|
+
# the phone may deliver less (measured on an iPhone 15 Pro: 28.5 fps at medium/30).
|
|
14
|
+
# One iproxy pair per phone: its WDA port and its MJPEG stream's port (8100 -> 9100, 8101 -> 9101).
|
|
15
|
+
WDA_BASE_PORT, MJPEG_BASE_PORT = 8100, 9100
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def default_mjpeg_url(wda_url):
|
|
19
|
+
"""The MJPEG URL paired with a WDA port (8100 -> 9100), or None when the port has no pair."""
|
|
20
|
+
parts = urlsplit(wda_url or "")
|
|
21
|
+
try:
|
|
22
|
+
port = parts.port # None without one: http's 80 or https's 443, which have no pair either
|
|
23
|
+
except ValueError:
|
|
24
|
+
return None
|
|
25
|
+
video = MJPEG_BASE_PORT + (port - WDA_BASE_PORT) if port is not None and port >= WDA_BASE_PORT else None
|
|
26
|
+
if not parts.hostname or video is None or video > 65535:
|
|
27
|
+
return None
|
|
28
|
+
host = f"[{parts.hostname}]" if ":" in parts.hostname else parts.hostname # an IPv6 address keeps its brackets
|
|
29
|
+
return urlunsplit((parts.scheme, f"{host}:{video}", "", "", ""))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
VIDEO_FPS = (15, 30, 60)
|
|
33
|
+
VIDEO_DETAIL = {"low": {"mjpegScalingFactor": 35, "mjpegServerScreenshotQuality": 40},
|
|
34
|
+
"medium": {"mjpegScalingFactor": 50, "mjpegServerScreenshotQuality": 50},
|
|
35
|
+
"high": {"mjpegScalingFactor": 75, "mjpegServerScreenshotQuality": 65}}
|
|
36
|
+
DEFAULT_QUALITY = (30, "medium")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def mjpeg_settings(fps, detail):
|
|
40
|
+
if fps not in VIDEO_FPS or detail not in VIDEO_DETAIL:
|
|
41
|
+
raise ValueError("Frame rate is 15, 30 or 60; detail is low, medium or high")
|
|
42
|
+
return {"mjpegServerFramerate": fps, **VIDEO_DETAIL[detail]}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
SCREENSHOT_FPS = 4
|
|
46
|
+
IDLE_GRACE_SECONDS = 3.0
|
|
47
|
+
MJPEG_RETRY_SECONDS = 5.0
|
|
48
|
+
# A stream silent this long is dropped and reopened. WDA sends no frame while it serves a long
|
|
49
|
+
# request (a simulator's 1,263-node source read, an XCTest drag of ~8 s); a stream that had
|
|
50
|
+
# delivered frames reconnects at once, since waiting MJPEG_RETRY_SECONDS left the FrameClock
|
|
51
|
+
# blind ~10 s after every such stall.
|
|
52
|
+
MJPEG_READ_SECONDS = 5.0
|
|
53
|
+
MAX_FRAME_BYTES = 8 * 1024 * 1024
|
|
54
|
+
MAX_HEADER_LINE = 1024
|
|
55
|
+
MAX_VIEWERS = 8
|
|
56
|
+
# How long a still picture (peek) waits for a fresh frame when the relay was stopped.
|
|
57
|
+
PEEK_WAIT_SECONDS = 2.0
|
|
58
|
+
BOUNDARY = "mobsterframe"
|
|
59
|
+
# A paced viewer is sent an unchanged screen again this often. The dashboard calls a picture with no frame for 2 s
|
|
60
|
+
# stale ("Last frame at ..."), so a still phone must never look like a stalled stream.
|
|
61
|
+
HEARTBEAT_SECONDS = 1.0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# WDA writes every frame it makes into its socket and never drops one, so on a link slower than the stream the frames
|
|
65
|
+
# queue on the phone and the picture falls further behind every second (13 Oct 2026, scripts/measure_live_link.py
|
|
66
|
+
# through a 4 Mbit/s link: 2.6 frames a second, each 25.7 s old after 30 s, 172 MB queued; with this, 6.2 frames a
|
|
67
|
+
# second, 0.15 s old). The relay times each frame's body as it arrives. On a link with room, a frame comes
|
|
68
|
+
# in a burst and the connection then waits for the next one (a USB iPhone: under 15% of the time). On a saturated
|
|
69
|
+
# link the next frame's bytes are already waiting, so receiving fills the whole time between frames. That share is
|
|
70
|
+
# the link's load: above LINK_BUSY the relay steps down a level and reconnects, which drops what queued on the phone;
|
|
71
|
+
# below LINK_IDLE for a while it steps back up. A phone that is itself slow (busy, hot) sends small bursts with long
|
|
72
|
+
# waits between them, so it never reads as a slow link.
|
|
73
|
+
LINK_STEPS = ((30, 50, 50), (20, 45, 45), (15, 40, 40), (10, 35, 40), (6, 30, 35), (3, 25, 30))
|
|
74
|
+
LINK_BUSY = 0.85
|
|
75
|
+
LINK_IDLE = 0.45
|
|
76
|
+
# Each reading covers at least this long and LINK_FRAMES frames.
|
|
77
|
+
LINK_WINDOW_SECONDS = 1.0
|
|
78
|
+
LINK_FRAMES = 3
|
|
79
|
+
# A step up waits this long at a low load, twice as long after one that had to come back down (to LINK_RECOVER_MAX).
|
|
80
|
+
LINK_RECOVER_SECONDS = 8.0
|
|
81
|
+
LINK_RECOVER_MAX = 64.0
|
|
82
|
+
# A step up that is still standing after this long resets the wait.
|
|
83
|
+
LINK_STABLE_SECONDS = 30.0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def link_ladder(settings):
|
|
87
|
+
"""The levels a link may use under ``settings`` (the user's choice, level 0)."""
|
|
88
|
+
top = (settings["mjpegServerFramerate"], settings["mjpegScalingFactor"], settings["mjpegServerScreenshotQuality"])
|
|
89
|
+
ladder = [top]
|
|
90
|
+
for step in LINK_STEPS:
|
|
91
|
+
level = tuple(min(a, b) for a, b in zip(step, top))
|
|
92
|
+
if level != ladder[-1] and all(a <= b for a, b in zip(level, ladder[-1])):
|
|
93
|
+
ladder.append(level)
|
|
94
|
+
return ladder
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class LinkGovernor:
|
|
98
|
+
"""The level a link can carry (0: the user's choice), from the share of time the relay spends receiving."""
|
|
99
|
+
|
|
100
|
+
def __init__(self, clock=time.monotonic):
|
|
101
|
+
self.clock = clock
|
|
102
|
+
self.level = 0
|
|
103
|
+
self.levels = 1
|
|
104
|
+
self.load = None
|
|
105
|
+
self.recover = LINK_RECOVER_SECONDS
|
|
106
|
+
self._reset_window()
|
|
107
|
+
self.idle_since = None
|
|
108
|
+
self.raised_at = None
|
|
109
|
+
|
|
110
|
+
def _reset_window(self):
|
|
111
|
+
self.busy = self.span = 0.0
|
|
112
|
+
self.count = 0
|
|
113
|
+
|
|
114
|
+
def reset(self, levels):
|
|
115
|
+
"""A new ladder (the user changed the picture): start again at its top."""
|
|
116
|
+
self.level, self.levels, self.load = 0, levels, None
|
|
117
|
+
self.recover, self.idle_since, self.raised_at = LINK_RECOVER_SECONDS, None, None
|
|
118
|
+
self._reset_window()
|
|
119
|
+
|
|
120
|
+
def restart(self):
|
|
121
|
+
"""A new connection: the first frames after it measure the connection, not the link."""
|
|
122
|
+
self._reset_window()
|
|
123
|
+
|
|
124
|
+
def frame(self, body, interval):
|
|
125
|
+
if interval is None or interval <= 0:
|
|
126
|
+
return None
|
|
127
|
+
self.busy += min(body, interval)
|
|
128
|
+
self.span += interval
|
|
129
|
+
self.count += 1
|
|
130
|
+
if self.span < LINK_WINDOW_SECONDS or self.count < LINK_FRAMES:
|
|
131
|
+
return None
|
|
132
|
+
load, now = self.busy / self.span, self.clock()
|
|
133
|
+
self.load = round(load, 2)
|
|
134
|
+
self._reset_window()
|
|
135
|
+
if self.raised_at is not None and now - self.raised_at >= LINK_STABLE_SECONDS:
|
|
136
|
+
self.raised_at, self.recover = None, LINK_RECOVER_SECONDS
|
|
137
|
+
if load > LINK_BUSY:
|
|
138
|
+
self.idle_since = None
|
|
139
|
+
if self.level + 1 >= self.levels:
|
|
140
|
+
return None
|
|
141
|
+
if self.raised_at is not None: # the last step up didn't hold: wait longer before the next
|
|
142
|
+
self.recover, self.raised_at = min(self.recover * 2, LINK_RECOVER_MAX), None
|
|
143
|
+
self.level += 1
|
|
144
|
+
return "down"
|
|
145
|
+
if load < LINK_IDLE and self.level > 0:
|
|
146
|
+
if self.idle_since is None:
|
|
147
|
+
self.idle_since = now
|
|
148
|
+
elif now - self.idle_since >= self.recover:
|
|
149
|
+
self.level -= 1
|
|
150
|
+
self.idle_since, self.raised_at = None, now
|
|
151
|
+
return "up"
|
|
152
|
+
return None
|
|
153
|
+
self.idle_since = None
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class VideoUnavailable(RuntimeError):
|
|
158
|
+
"""No viewer slot, or the relay is closed."""
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def read_mjpeg_frames(stream, on_body=None, clock=time.monotonic):
|
|
162
|
+
"""Yield JPEG frames from a multipart MJPEG byte stream (WDA's format)."""
|
|
163
|
+
while True:
|
|
164
|
+
headers = {}
|
|
165
|
+
line = stream.readline(MAX_HEADER_LINE)
|
|
166
|
+
if not line:
|
|
167
|
+
return
|
|
168
|
+
if not line.strip() or line.startswith(b"--"):
|
|
169
|
+
continue
|
|
170
|
+
while line and line.strip():
|
|
171
|
+
if b":" in line:
|
|
172
|
+
name, _, value = line.decode("latin-1").partition(":")
|
|
173
|
+
headers[name.strip().casefold()] = value.strip()
|
|
174
|
+
line = stream.readline(MAX_HEADER_LINE)
|
|
175
|
+
if not line:
|
|
176
|
+
return
|
|
177
|
+
length = headers.get("content-length")
|
|
178
|
+
if length is not None and length.isdigit():
|
|
179
|
+
size = int(length)
|
|
180
|
+
if size <= 0 or size > MAX_FRAME_BYTES:
|
|
181
|
+
raise ValueError("MJPEG frame size out of range")
|
|
182
|
+
started = clock()
|
|
183
|
+
data = stream.read(size)
|
|
184
|
+
if len(data) != size:
|
|
185
|
+
return
|
|
186
|
+
if on_body is not None:
|
|
187
|
+
on_body(clock() - started)
|
|
188
|
+
yield data
|
|
189
|
+
else:
|
|
190
|
+
buffer = bytearray()
|
|
191
|
+
while b"\xff\xd9" not in buffer:
|
|
192
|
+
chunk = stream.read(4096)
|
|
193
|
+
if not chunk:
|
|
194
|
+
return
|
|
195
|
+
buffer += chunk
|
|
196
|
+
if len(buffer) > MAX_FRAME_BYTES:
|
|
197
|
+
raise ValueError("MJPEG frame too large")
|
|
198
|
+
end = buffer.index(b"\xff\xd9") + 2
|
|
199
|
+
yield bytes(buffer[:end])
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class WdaVideo:
|
|
203
|
+
def __init__(self, wda_url, mjpeg_url=None, session=None, on_activity=None, clock=time.monotonic,
|
|
204
|
+
quality=DEFAULT_QUALITY, screenshot_fallback=True):
|
|
205
|
+
self.wda_url = wda_url.rstrip("/")
|
|
206
|
+
self.settings = mjpeg_settings(*quality) # the user's choice; a slow link uses a level below it
|
|
207
|
+
self.configured = None # (session, settings) last applied successfully
|
|
208
|
+
self.quality_changed = threading.Event() # set when the relay worker should send the settings again
|
|
209
|
+
self.link = LinkGovernor(clock)
|
|
210
|
+
self.link.reset(len(link_ladder(self.settings)))
|
|
211
|
+
if not mjpeg_url:
|
|
212
|
+
# The stream paired with this WDA's port (8100 -> 9100, a simulator's 8203 -> 9203): a caller
|
|
213
|
+
# that names none must never watch, and settle on, another device's screen. A port with no pair
|
|
214
|
+
# (8080) has no stream: screenshots only, or none for a private consumer.
|
|
215
|
+
mjpeg_url = default_mjpeg_url(self.wda_url)
|
|
216
|
+
self.mjpeg_url = mjpeg_url.rstrip("/") if mjpeg_url else None
|
|
217
|
+
self.session = session # callable returning the current WDA session id, or None
|
|
218
|
+
self.on_activity = on_activity or (lambda active: None)
|
|
219
|
+
self.clock = clock
|
|
220
|
+
# Off for private consumers (the FrameClock): polling /screenshot would
|
|
221
|
+
# queue behind, and delay, the agent's own WDA requests.
|
|
222
|
+
self.screenshot_fallback = screenshot_fallback
|
|
223
|
+
self.condition = threading.Condition()
|
|
224
|
+
self.viewers = set()
|
|
225
|
+
# (sequence, content_type, bytes, captured_at_ms wall clock, arrival monotonic, source)
|
|
226
|
+
self.frame = None
|
|
227
|
+
self.sequence = 0
|
|
228
|
+
self.source = None # "wda_mjpeg" | "wda_screenshot"
|
|
229
|
+
self.error = None
|
|
230
|
+
self.worker = None
|
|
231
|
+
self.closed = False
|
|
232
|
+
self.idle_since = None
|
|
233
|
+
self.frame_times = []
|
|
234
|
+
self._streamed = 0 # frames the current MJPEG connection delivered
|
|
235
|
+
|
|
236
|
+
def status(self):
|
|
237
|
+
with self.condition:
|
|
238
|
+
live = self.frame is not None and self.clock() - self.frame[4] < 3
|
|
239
|
+
return {"status": "live" if live else "starting" if self.viewers else "idle",
|
|
240
|
+
"kind": "mjpeg", "sourceKind": self.source, "error": self.error,
|
|
241
|
+
"viewers": len(self.viewers), "fps": round(self.fps(), 1),
|
|
242
|
+
"targetFps": self.settings["mjpegServerFramerate"],
|
|
243
|
+
"reducedFps": self.reduced_fps(), "linkLoad": self.link.load,
|
|
244
|
+
"capturedAt": self.frame[3] if self.frame else None,
|
|
245
|
+
"stream": "/api/device/stream"}
|
|
246
|
+
|
|
247
|
+
def fps(self):
|
|
248
|
+
now = self.clock()
|
|
249
|
+
recent = [t for t in self.frame_times if now - t <= 2]
|
|
250
|
+
return len(recent) / 2
|
|
251
|
+
|
|
252
|
+
def reduced_fps(self):
|
|
253
|
+
"""The frame rate a slow link brought the stream down to, or None at the user's own choice."""
|
|
254
|
+
with self.condition:
|
|
255
|
+
if not self.link.level:
|
|
256
|
+
return None
|
|
257
|
+
return link_ladder(self.settings)[self.link.level][0]
|
|
258
|
+
|
|
259
|
+
def applied_settings(self):
|
|
260
|
+
"""The settings WDA should stream with now: the user's choice, or the level a slow link holds."""
|
|
261
|
+
with self.condition:
|
|
262
|
+
if not self.link.level:
|
|
263
|
+
return dict(self.settings)
|
|
264
|
+
fps, scaling, quality = link_ladder(self.settings)[self.link.level]
|
|
265
|
+
return {**self.settings, "mjpegServerFramerate": fps, "mjpegScalingFactor": scaling,
|
|
266
|
+
"mjpegServerScreenshotQuality": quality}
|
|
267
|
+
|
|
268
|
+
def subscribe(self):
|
|
269
|
+
with self.condition:
|
|
270
|
+
if self.closed:
|
|
271
|
+
raise VideoUnavailable("Live video is shutting down")
|
|
272
|
+
if len(self.viewers) >= MAX_VIEWERS:
|
|
273
|
+
raise VideoUnavailable("Too many live viewers")
|
|
274
|
+
viewer = object()
|
|
275
|
+
self.viewers.add(viewer)
|
|
276
|
+
self.idle_since = None
|
|
277
|
+
if self.worker is None or not self.worker.is_alive():
|
|
278
|
+
self._start_worker()
|
|
279
|
+
return viewer
|
|
280
|
+
|
|
281
|
+
def _start_worker(self):
|
|
282
|
+
# The caller holds the condition, also during the activity notification.
|
|
283
|
+
self.worker = threading.Thread(target=self._run, name="mobster-wda-video", daemon=True)
|
|
284
|
+
self.worker.start()
|
|
285
|
+
self.on_activity(True)
|
|
286
|
+
|
|
287
|
+
def unsubscribe(self, viewer):
|
|
288
|
+
with self.condition:
|
|
289
|
+
self.viewers.discard(viewer)
|
|
290
|
+
if not self.viewers:
|
|
291
|
+
self.idle_since = self.clock()
|
|
292
|
+
self.condition.notify_all()
|
|
293
|
+
|
|
294
|
+
def next_frame(self, viewer, after, timeout=5.0):
|
|
295
|
+
"""The newest frame newer than sequence ``after``, or None on timeout/close.
|
|
296
|
+
The FrameClock reads every frame here, identical ones included: its "the screen is still" answers count them."""
|
|
297
|
+
deadline = self.clock() + timeout
|
|
298
|
+
with self.condition:
|
|
299
|
+
while not self.closed and viewer in self.viewers:
|
|
300
|
+
if self.frame is not None and self.frame[0] > after:
|
|
301
|
+
return self.frame
|
|
302
|
+
remaining = deadline - self.clock()
|
|
303
|
+
if remaining <= 0:
|
|
304
|
+
return None
|
|
305
|
+
self.condition.wait(remaining)
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
def latest(self):
|
|
309
|
+
with self.condition:
|
|
310
|
+
return self.frame
|
|
311
|
+
|
|
312
|
+
def peek(self, wait=PEEK_WAIT_SECONDS):
|
|
313
|
+
"""The newest frame for a still picture (a tile); starts a stopped relay and waits up to ``wait`` for one."""
|
|
314
|
+
try:
|
|
315
|
+
viewer = self.subscribe()
|
|
316
|
+
except VideoUnavailable:
|
|
317
|
+
return self.latest()
|
|
318
|
+
try:
|
|
319
|
+
deadline = self.clock() + wait
|
|
320
|
+
with self.condition:
|
|
321
|
+
while not self.closed and (self.frame is None or self.clock() - self.frame[4] > IDLE_GRACE_SECONDS):
|
|
322
|
+
remaining = deadline - self.clock()
|
|
323
|
+
if remaining <= 0:
|
|
324
|
+
break
|
|
325
|
+
self.condition.wait(remaining)
|
|
326
|
+
return self.frame
|
|
327
|
+
finally:
|
|
328
|
+
self.unsubscribe(viewer)
|
|
329
|
+
|
|
330
|
+
def paced_frames(self, viewer, rate=lambda: None, heartbeat=HEARTBEAT_SECONDS, timeout=10.0):
|
|
331
|
+
"""The frames one viewer is sent, until the relay closes or the viewer unsubscribes."""
|
|
332
|
+
sequence, sent, sent_at, looked_at = 0, None, None, None
|
|
333
|
+
while True:
|
|
334
|
+
fps = rate()
|
|
335
|
+
if isinstance(fps, (int, float)) and fps > 0 and looked_at is not None:
|
|
336
|
+
wait = looked_at + 1 / fps - self.clock()
|
|
337
|
+
if wait > 0 and self._sleep(viewer, wait):
|
|
338
|
+
return
|
|
339
|
+
frame = self.next_frame(viewer, sequence, timeout=timeout)
|
|
340
|
+
if frame is None:
|
|
341
|
+
with self.condition:
|
|
342
|
+
if self.closed or viewer not in self.viewers:
|
|
343
|
+
return
|
|
344
|
+
continue # no new frame yet: the stream is starting or stalled
|
|
345
|
+
sequence, now = frame[0], self.clock()
|
|
346
|
+
looked_at = now
|
|
347
|
+
if (sent is not None and frame[2] == sent[2] and frame[1] == sent[1]
|
|
348
|
+
and now - sent_at < heartbeat):
|
|
349
|
+
continue
|
|
350
|
+
sent, sent_at = frame, now
|
|
351
|
+
yield frame
|
|
352
|
+
|
|
353
|
+
def _sleep(self, viewer, seconds):
|
|
354
|
+
"""Sleep ``seconds`` (the condition wakes on every frame); True when the relay closed or the viewer left."""
|
|
355
|
+
time.sleep(seconds)
|
|
356
|
+
with self.condition:
|
|
357
|
+
return self.closed or viewer not in self.viewers
|
|
358
|
+
|
|
359
|
+
def release_idle(self):
|
|
360
|
+
"""Let go of the phone: end every stream and stop the worker; a viewer that subscribes again starts it."""
|
|
361
|
+
with self.condition:
|
|
362
|
+
self.viewers.clear()
|
|
363
|
+
self.idle_since = self.clock() - IDLE_GRACE_SECONDS
|
|
364
|
+
self.frame = None
|
|
365
|
+
self.condition.notify_all()
|
|
366
|
+
|
|
367
|
+
def close(self):
|
|
368
|
+
with self.condition:
|
|
369
|
+
self.closed = True
|
|
370
|
+
self.condition.notify_all()
|
|
371
|
+
|
|
372
|
+
def _publish(self, content_type, data, source):
|
|
373
|
+
now = self.clock()
|
|
374
|
+
with self.condition:
|
|
375
|
+
self.sequence += 1
|
|
376
|
+
self.frame = (self.sequence, content_type, data, int(time.time() * 1000), now, source)
|
|
377
|
+
self.source, self.error = source, None
|
|
378
|
+
self.frame_times = [t for t in self.frame_times if now - t <= 2] + [now]
|
|
379
|
+
self.condition.notify_all()
|
|
380
|
+
|
|
381
|
+
def _should_run(self):
|
|
382
|
+
with self.condition:
|
|
383
|
+
if self.closed:
|
|
384
|
+
return False
|
|
385
|
+
if self.viewers:
|
|
386
|
+
return True
|
|
387
|
+
return self.idle_since is not None and self.clock() - self.idle_since < IDLE_GRACE_SECONDS
|
|
388
|
+
|
|
389
|
+
def set_quality(self, fps, detail):
|
|
390
|
+
"""Change frame rate and detail; WDA applies them to a running stream."""
|
|
391
|
+
settings = mjpeg_settings(fps, detail)
|
|
392
|
+
with self.condition:
|
|
393
|
+
if self.settings == settings:
|
|
394
|
+
return
|
|
395
|
+
self.settings = settings
|
|
396
|
+
self.link.reset(len(link_ladder(settings)))
|
|
397
|
+
self.quality_changed.set()
|
|
398
|
+
|
|
399
|
+
def _configure(self):
|
|
400
|
+
"""Apply the quality settings; True once WDA accepted them for its session, else retried before each reconnect.
|
|
401
|
+
Only the relay worker sends them, so an older request can't land after a newer one."""
|
|
402
|
+
self.quality_changed.clear()
|
|
403
|
+
try:
|
|
404
|
+
session = self.session() if self.session else None
|
|
405
|
+
except Exception:
|
|
406
|
+
session = None
|
|
407
|
+
if not session:
|
|
408
|
+
return False
|
|
409
|
+
settings = self.applied_settings()
|
|
410
|
+
key = (session, tuple(sorted(settings.items())))
|
|
411
|
+
if self.configured == key:
|
|
412
|
+
return True
|
|
413
|
+
try:
|
|
414
|
+
self._request(self.wda_url, "POST", f"/session/{session}/appium/settings",
|
|
415
|
+
{"settings": settings}, timeout=5)
|
|
416
|
+
except Exception:
|
|
417
|
+
return False
|
|
418
|
+
self.configured = key
|
|
419
|
+
return True
|
|
420
|
+
|
|
421
|
+
def _run(self):
|
|
422
|
+
try:
|
|
423
|
+
while self._should_run():
|
|
424
|
+
self._configure() # a no-op once applied for this session and quality
|
|
425
|
+
self._streamed = 0
|
|
426
|
+
try:
|
|
427
|
+
if self.mjpeg_url:
|
|
428
|
+
self._stream_mjpeg()
|
|
429
|
+
except Exception as error:
|
|
430
|
+
with self.condition:
|
|
431
|
+
self.error = type(error).__name__
|
|
432
|
+
if self._streamed:
|
|
433
|
+
continue # it was streaming: a stall, not an absent server
|
|
434
|
+
retry_at = self.clock() + MJPEG_RETRY_SECONDS
|
|
435
|
+
while self._should_run() and self.clock() < retry_at:
|
|
436
|
+
if self.quality_changed.is_set():
|
|
437
|
+
self._configure()
|
|
438
|
+
if not self.screenshot_fallback:
|
|
439
|
+
time.sleep(.25)
|
|
440
|
+
elif not self._screenshot_once():
|
|
441
|
+
time.sleep(.5)
|
|
442
|
+
else:
|
|
443
|
+
time.sleep(1 / SCREENSHOT_FPS)
|
|
444
|
+
finally:
|
|
445
|
+
with self.condition:
|
|
446
|
+
self.worker = None
|
|
447
|
+
self.frame = None
|
|
448
|
+
self.frame_times = []
|
|
449
|
+
self.on_activity(False)
|
|
450
|
+
# A viewer may subscribe after the last _should_run() check and before this cleanup, while the
|
|
451
|
+
# old worker is still alive. Hand it to a new worker before the lock is released.
|
|
452
|
+
if self.viewers and not self.closed:
|
|
453
|
+
self._start_worker()
|
|
454
|
+
|
|
455
|
+
def _stream_mjpeg(self):
|
|
456
|
+
parts = urlsplit(self.mjpeg_url)
|
|
457
|
+
client = http.client.HTTPSConnection if parts.scheme == "https" else http.client.HTTPConnection
|
|
458
|
+
connection = client(parts.hostname, parts.port, timeout=MJPEG_READ_SECONDS)
|
|
459
|
+
try:
|
|
460
|
+
connection.request("GET", parts.path or "/")
|
|
461
|
+
response = connection.getresponse()
|
|
462
|
+
if response.status != 200:
|
|
463
|
+
raise ConnectionError(f"MJPEG server answered {response.status}")
|
|
464
|
+
self.link.restart()
|
|
465
|
+
body, ended = [0.0], [None]
|
|
466
|
+
for data in read_mjpeg_frames(response, on_body=lambda seconds: body.__setitem__(0, seconds),
|
|
467
|
+
clock=self.clock):
|
|
468
|
+
self._streamed += 1
|
|
469
|
+
self._publish("image/jpeg", data, "wda_mjpeg")
|
|
470
|
+
now = self.clock()
|
|
471
|
+
with self.condition:
|
|
472
|
+
step = self.link.frame(body[0], now - ended[0] if ended[0] is not None else None)
|
|
473
|
+
ended[0] = now
|
|
474
|
+
if step == "down":
|
|
475
|
+
return # _run applies the lower level and reconnects, dropping what queued on the phone
|
|
476
|
+
if step == "up":
|
|
477
|
+
self.quality_changed.set()
|
|
478
|
+
if not self._should_run():
|
|
479
|
+
return
|
|
480
|
+
if self.quality_changed.is_set():
|
|
481
|
+
self._configure()
|
|
482
|
+
finally:
|
|
483
|
+
connection.close()
|
|
484
|
+
|
|
485
|
+
def _screenshot_once(self):
|
|
486
|
+
try:
|
|
487
|
+
value = self._request(self.wda_url, "GET", "/screenshot", timeout=5).get("value")
|
|
488
|
+
data = base64.b64decode(value, validate=True)
|
|
489
|
+
except Exception as error:
|
|
490
|
+
with self.condition:
|
|
491
|
+
self.error = type(error).__name__
|
|
492
|
+
return False
|
|
493
|
+
content_type = "image/png" if data.startswith(b"\x89PNG") else "image/jpeg"
|
|
494
|
+
self._publish(content_type, data, "wda_screenshot")
|
|
495
|
+
return True
|
|
496
|
+
|
|
497
|
+
@staticmethod
|
|
498
|
+
def _request(base, method, path, body=None, timeout=5):
|
|
499
|
+
"""One bounded WDA request: the deadline covers headers and body, so a dripping phone can't stretch it."""
|
|
500
|
+
from ..transport import HTTP
|
|
501
|
+
client = HTTP(base)
|
|
502
|
+
try:
|
|
503
|
+
return client.request(method, path, body, timeout=wda_timeouts.bounded(timeout, method, path))
|
|
504
|
+
finally:
|
|
505
|
+
client.close()
|