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,512 @@
|
|
|
1
|
+
"""Photos and videos between the Mac and the iPhone's Photos library.
|
|
2
|
+
|
|
3
|
+
**Mac to iPhone (``send``).** iOS has no way to add to Photos from a Mac: files copied into DCIM over AFC sit on disk
|
|
4
|
+
and Photos never shows them (discuss.appium.io/t/28882). So Mobster's own runner on the phone (the WebDriverAgent it
|
|
5
|
+
builds, patched: wda.patches.WDA_PHOTOS_COMMANDS) saves the file with PhotoKit, as any app that adds to Photos
|
|
6
|
+
does. The file goes over the runner's connection as base64 (at most 100 MB). The first time, iOS asks whether the
|
|
7
|
+
runner may use Photos; "Limit Access" is enough, since what an app adds joins its selection. A runner built before
|
|
8
|
+
this support answers 404: rebuild it once in Setup.
|
|
9
|
+
|
|
10
|
+
**iPhone to Mac (``pull``).** The newest photos, screenshots and screen recordings come from the camera roll's folder
|
|
11
|
+
(DCIM) through Apple's file service on the cable (AFC, the media root: ``afcclient`` without ``--documents``), newest
|
|
12
|
+
first, by count or since a time. With iCloud Photos' Optimize iPhone Storage on, older originals live in iCloud and
|
|
13
|
+
only what is on the phone comes back. Edits (.AAE sidecars) and folders are skipped; a HEIC can be converted to JPEG
|
|
14
|
+
on the Mac with ``sips``.
|
|
15
|
+
|
|
16
|
+
Who approves: the agent asks before it adds to Photos (PUT_FILE's destination ``photos``), the Mac app's drop on the
|
|
17
|
+
phone is the user's own gesture, ``mobster phone photos`` is the user's command, and MCP lists ``send_media`` only
|
|
18
|
+
with ``--allow-files`` and ``pull_media`` only with ``--allow-photos`` (the camera roll is the user's most private
|
|
19
|
+
folder).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import base64
|
|
23
|
+
import hashlib
|
|
24
|
+
import os
|
|
25
|
+
import re
|
|
26
|
+
import shutil
|
|
27
|
+
import subprocess
|
|
28
|
+
import tempfile
|
|
29
|
+
import time
|
|
30
|
+
import uuid
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
from . import PhoneIOError
|
|
34
|
+
|
|
35
|
+
MAX_SEND_BYTES = 100 * 1024 * 1024 # a runner built before transfers: one piece
|
|
36
|
+
MAX_CHUNKED_BYTES = 4 * 1024 ** 3 # a runner built for transfers: in pieces
|
|
37
|
+
CHUNK_BYTES = 8 * 1024 * 1024
|
|
38
|
+
MAX_RECENT = 50
|
|
39
|
+
MAX_PULL = 20
|
|
40
|
+
MAX_PULL_BYTES = 500 * 1024 * 1024
|
|
41
|
+
AUTHORIZATION_WAIT_S = 60
|
|
42
|
+
AUTHORIZE_WAIT_S = 20 # Setup's "Allow": how long the helper waits for the person to answer iOS's question
|
|
43
|
+
PHOTO_TYPES = {"jpg": "photo", "jpeg": "photo", "heic": "photo", "heif": "photo", "png": "photo", "gif": "photo",
|
|
44
|
+
"webp": "photo", "tif": "photo", "tiff": "photo", "dng": "photo",
|
|
45
|
+
"mov": "video", "mp4": "video", "m4v": "video", "hevc": "video"}
|
|
46
|
+
# Magic numbers: a file is what its first bytes say, not its name.
|
|
47
|
+
SIGNATURES = ((b"\xff\xd8\xff", "jpg"), (b"\x89PNG\r\n\x1a\n", "png"), (b"GIF8", "gif"), (b"II*\x00", "tif"),
|
|
48
|
+
(b"MM\x00*", "tif"))
|
|
49
|
+
MEDIA_ROOT = "/DCIM"
|
|
50
|
+
DCIM_FOLDER = re.compile(r"^\d{3}[A-Z_]{0,12}$")
|
|
51
|
+
PULLED = re.compile(r"\.(heic|heif|jpe?g|png|gif|mov|mp4|m4v|dng|webp)$", re.I)
|
|
52
|
+
NEEDS_REBUILD = ("Mobster's runner on {name} was built before it could add to Photos. Rebuild it once in Setup › "
|
|
53
|
+
"Runner (about 2 minutes), then try again.")
|
|
54
|
+
STATUS_WORDS = {
|
|
55
|
+
"denied": "Photos access is off for Mobster on {name}. Turn it on in Settings › Privacy & Security › Photos › "
|
|
56
|
+
"WebDriverAgentRunner-Runner (Limit Access is enough), then try again.",
|
|
57
|
+
"restricted": "Photos is restricted on {name} (Screen Time or a profile), so Mobster can't add to it.",
|
|
58
|
+
"not_determined": "{name} is asking whether Mobster may use Photos. Answer it on the iPhone (Limit Access is "
|
|
59
|
+
"enough), then try again.",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def sniff(path):
|
|
64
|
+
"""("photo" | "video", extension) for a Mac file, from its first bytes (and, for video and HEIC, its box)."""
|
|
65
|
+
file = Path(path).expanduser()
|
|
66
|
+
try:
|
|
67
|
+
with open(file, "rb") as stream:
|
|
68
|
+
head = stream.read(32)
|
|
69
|
+
except OSError:
|
|
70
|
+
raise PhoneIOError(f"There is no file at {file}.", "usage") from None
|
|
71
|
+
for magic, ext in SIGNATURES:
|
|
72
|
+
if head.startswith(magic):
|
|
73
|
+
return "photo", ext
|
|
74
|
+
if head[:4] == b"RIFF" and head[8:12] == b"WEBP":
|
|
75
|
+
return "photo", "webp"
|
|
76
|
+
if head[4:8] == b"ftyp":
|
|
77
|
+
brand = head[8:12].decode("ascii", "replace").lower()
|
|
78
|
+
if brand in ("heic", "heix", "hevc", "heim", "heis", "mif1", "msf1"):
|
|
79
|
+
return "photo", "heic"
|
|
80
|
+
if brand.startswith("qt"):
|
|
81
|
+
return "video", "mov"
|
|
82
|
+
return "video", "mp4"
|
|
83
|
+
raise PhoneIOError(f"{file.name} isn't a photo or a video Photos takes (JPEG, HEIC, PNG, GIF, WebP, MOV or MP4).",
|
|
84
|
+
"usage")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _runner_answer(request, method, path, body=None, timeout=15, *, name="the iPhone"):
|
|
88
|
+
"""``request``'s value, or the PhoneIOError a runner without the Photos routes deserves."""
|
|
89
|
+
try:
|
|
90
|
+
return request(method, path, body, timeout)
|
|
91
|
+
except PhoneIOError as error:
|
|
92
|
+
if error.code == "unknown_route":
|
|
93
|
+
raise PhoneIOError(NEEDS_REBUILD.format(name=name), "needs_rebuild") from None
|
|
94
|
+
raise
|
|
95
|
+
except Exception as error:
|
|
96
|
+
text = str(error)
|
|
97
|
+
if "404" in text or "unknown command" in text.casefold() or "Unhandled endpoint" in text:
|
|
98
|
+
raise PhoneIOError(NEEDS_REBUILD.format(name=name), "needs_rebuild") from None
|
|
99
|
+
raise PhoneIOError(f"Mobster's runner didn't answer ({type(error).__name__}).", "failed",
|
|
100
|
+
"Keep the iPhone unlocked and connected, then try again.") from None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def status(request, name="the iPhone"):
|
|
104
|
+
"""{"status": authorized | limited | denied | restricted | not_determined, "bundleId": the runner's}."""
|
|
105
|
+
value = _runner_answer(request, "GET", "/mobster/photos/status", None, 10, name=name)
|
|
106
|
+
return value if isinstance(value, dict) else {"status": "not_determined", "bundleId": ""}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def authorize(request, *, name="the iPhone", activate=None):
|
|
110
|
+
"""Ask iOS to show its Photos question on the phone and wait AUTHORIZE_WAIT_S for the person to answer it there
|
|
111
|
+
(``POST /mobster/photos/authorize``). It saves nothing, asks only while the status is ``not_determined``, never
|
|
112
|
+
presses the grant and never resets an answer. ``activate(bundle)`` brings the helper forward so the question shows.
|
|
113
|
+
Returns {"status", "asked"}; PhoneIOError ``needs_rebuild`` from a helper built without the route."""
|
|
114
|
+
state = status(request, name)
|
|
115
|
+
if state.get("status") != "not_determined":
|
|
116
|
+
return {"status": state.get("status"), "asked": False}
|
|
117
|
+
_forward(state, activate, None)
|
|
118
|
+
answer = _runner_answer(request, "POST", "/mobster/photos/authorize", {"authorizationTimeout": AUTHORIZE_WAIT_S},
|
|
119
|
+
AUTHORIZE_WAIT_S + 15, name=name)
|
|
120
|
+
answer = answer if isinstance(answer, dict) else {}
|
|
121
|
+
return {"status": answer.get("status") or "not_determined", "asked": bool(answer.get("asked"))}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def digest(path, chunk=1 << 20):
|
|
125
|
+
"""The SHA-256 of the Mac file ``path``, hex."""
|
|
126
|
+
hashed = hashlib.sha256()
|
|
127
|
+
with open(path, "rb") as stream:
|
|
128
|
+
for block in iter(lambda: stream.read(chunk), b""):
|
|
129
|
+
hashed.update(block)
|
|
130
|
+
return hashed.hexdigest()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def count(request, name="the iPhone"):
|
|
134
|
+
try:
|
|
135
|
+
value = _runner_answer(request, "GET", "/mobster/photos/count", None, 20, name=name)
|
|
136
|
+
except PhoneIOError as error:
|
|
137
|
+
if error.code == "needs_rebuild":
|
|
138
|
+
return None
|
|
139
|
+
raise
|
|
140
|
+
if not isinstance(value, dict) or "photos" not in value:
|
|
141
|
+
return None if not isinstance(value, dict) or "status" not in value else {"status": value["status"]}
|
|
142
|
+
return {"photos": int(value.get("photos") or 0), "videos": int(value.get("videos") or 0),
|
|
143
|
+
"status": value.get("status")}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _forward(state, activate, front):
|
|
147
|
+
"""Bring the runner forward for iOS's first Photos question; the bundle that was in front, to give back."""
|
|
148
|
+
if state.get("status") != "not_determined" or activate is None or not state.get("bundleId"):
|
|
149
|
+
return None
|
|
150
|
+
try:
|
|
151
|
+
origin = front() if front is not None else None
|
|
152
|
+
activate(state["bundleId"])
|
|
153
|
+
return origin
|
|
154
|
+
except Exception:
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _refused(answer, file, name):
|
|
159
|
+
words = STATUS_WORDS.get(answer.get("status"))
|
|
160
|
+
if words:
|
|
161
|
+
return PhoneIOError(words.format(name=name), "photos_" + answer["status"])
|
|
162
|
+
return PhoneIOError(f"Photos didn't take {file.name}" + (f": {answer['error']}" if answer.get("error") else "")
|
|
163
|
+
+ ".", "failed", "Check that it opens on this Mac, then try again.")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def send(request, path, *, name="the iPhone", activate=None, front=None, clock=time.monotonic, album=None,
|
|
167
|
+
progress=None, chunk=CHUNK_BYTES, sha256=None, date_now=False, before_send=None):
|
|
168
|
+
"""Add the Mac file ``path`` to Photos (and the album ``album``, made if missing). ``request(method, path, body,
|
|
169
|
+
timeout)`` is a WebDriverAgent call (phone_io.wda's, or a driver's). ``activate(bundle)``/``front()`` bring the
|
|
170
|
+
runner forward for iOS's first question and the app that was in front back after (best effort).
|
|
171
|
+
``progress(sent, total)`` follows the bytes. ``date_now``: dated now in the library (the file is unchanged), so
|
|
172
|
+
it comes first in every app's Recents.
|
|
173
|
+
|
|
174
|
+
A runner built for transfers takes the file in pieces of ``chunk`` bytes and checks its SHA-256 against the Mac's
|
|
175
|
+
before Photos sees it (``verified``); an older one takes it in one piece of at most 100 MB with no album, as
|
|
176
|
+
before. Returns {"kind", "bytes", "name", "localIdentifier", "status", "seconds", "sha256", "verified", "album",
|
|
177
|
+
"albumError"}. Sending is never retried here: a save whose answer is lost raises ``unknown`` (it may be in
|
|
178
|
+
Photos). ``before_send()`` runs after preflight and before any activation or transfer;
|
|
179
|
+
a refusal prevents sending."""
|
|
180
|
+
from .files import snapshot
|
|
181
|
+
started = clock()
|
|
182
|
+
with snapshot(path, MAX_CHUNKED_BYTES) as file:
|
|
183
|
+
size = file.stat().st_size
|
|
184
|
+
if size == 0:
|
|
185
|
+
raise PhoneIOError(f"{file.name} is empty.", "usage")
|
|
186
|
+
kind, ext = sniff(file)
|
|
187
|
+
checksum = digest(file)
|
|
188
|
+
if sha256 is not None and checksum != sha256:
|
|
189
|
+
raise PhoneIOError(f"{file.name} changed before it could be sent. Nothing was added to Photos.", "checksum")
|
|
190
|
+
state = status(request, name)
|
|
191
|
+
if before_send is not None:
|
|
192
|
+
before_send()
|
|
193
|
+
origin = _forward(state, activate, front)
|
|
194
|
+
try:
|
|
195
|
+
answer = _send_pieces(request, file, size, kind, ext, checksum, album, progress, chunk, name, date_now)
|
|
196
|
+
legacy = answer is None
|
|
197
|
+
if legacy:
|
|
198
|
+
if size > MAX_SEND_BYTES:
|
|
199
|
+
raise PhoneIOError(f"Mobster's runner on {name} takes files up to 100 MB. Rebuild it once in "
|
|
200
|
+
"Setup › Runner (about 2 minutes) to send larger ones.", "needs_rebuild")
|
|
201
|
+
data = base64.b64encode(file.read_bytes()).decode("ascii")
|
|
202
|
+
try:
|
|
203
|
+
answer = _runner_answer(request, "POST", "/mobster/photos",
|
|
204
|
+
{"data": data, "kind": kind, "extension": ext, "filename": file.name[:120],
|
|
205
|
+
"authorizationTimeout": AUTHORIZATION_WAIT_S},
|
|
206
|
+
AUTHORIZATION_WAIT_S + 60 + size / 2_000_000, name=name)
|
|
207
|
+
except PhoneIOError as error:
|
|
208
|
+
if error.code == "failed":
|
|
209
|
+
raise _lost(file, name) from None
|
|
210
|
+
raise
|
|
211
|
+
if progress is not None:
|
|
212
|
+
progress(size, size)
|
|
213
|
+
finally:
|
|
214
|
+
if origin and activate is not None:
|
|
215
|
+
try:
|
|
216
|
+
activate(origin)
|
|
217
|
+
except Exception:
|
|
218
|
+
pass
|
|
219
|
+
answer = answer if isinstance(answer, dict) else {}
|
|
220
|
+
if answer.get("error") == "checksum":
|
|
221
|
+
raise PhoneIOError(f"{file.name} didn't arrive whole on {name}: its checksum on the iPhone differs from "
|
|
222
|
+
"this Mac's. Nothing was added to Photos.", "checksum", "Try again.")
|
|
223
|
+
if not answer.get("saved"):
|
|
224
|
+
raise _refused(answer, file, name)
|
|
225
|
+
album_error = answer.get("albumError") or None
|
|
226
|
+
if album and legacy:
|
|
227
|
+
album_error = "Mobster's runner was built before albums. Rebuild it once in Setup › Runner."
|
|
228
|
+
return {"kind": kind, "bytes": size, "name": file.name,
|
|
229
|
+
"localIdentifier": answer.get("localIdentifier") or None,
|
|
230
|
+
"status": answer.get("status"), "seconds": round(clock() - started, 2), "sha256": checksum,
|
|
231
|
+
"verified": not legacy and answer.get("sha256") == checksum, "album": album if album and not album_error
|
|
232
|
+
else None, "albumError": album_error}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _lost(file, name):
|
|
236
|
+
return PhoneIOError(f"{name} stopped answering while it saved {file.name}, so it may or may not be in Photos.",
|
|
237
|
+
"unknown", "Look in Photos on the iPhone before you send it again.")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _send_pieces(request, file, size, kind, ext, checksum, album, progress, chunk, name, date_now=False):
|
|
241
|
+
"""The runner's answer to the save, or None when the runner has no piece route (built before transfers)."""
|
|
242
|
+
upload = uuid.uuid4().hex[:16]
|
|
243
|
+
sent = 0
|
|
244
|
+
resumed = False
|
|
245
|
+
with open(file, "rb") as stream:
|
|
246
|
+
while sent < size:
|
|
247
|
+
stream.seek(sent)
|
|
248
|
+
block = stream.read(chunk)
|
|
249
|
+
try:
|
|
250
|
+
answer = _runner_answer(request, "POST", "/mobster/photos/chunk",
|
|
251
|
+
{"upload": upload, "offset": sent,
|
|
252
|
+
"data": base64.b64encode(block).decode("ascii")},
|
|
253
|
+
60 + len(block) / 1_000_000, name=name)
|
|
254
|
+
except PhoneIOError as error:
|
|
255
|
+
if error.code == "needs_rebuild" and sent == 0:
|
|
256
|
+
return None
|
|
257
|
+
raise
|
|
258
|
+
if sent == 0 and (not isinstance(answer, dict) or "ok" not in answer):
|
|
259
|
+
return None # not the piece route's answer: a runner built before transfers
|
|
260
|
+
if not isinstance(answer, dict) or not answer.get("ok"):
|
|
261
|
+
have = answer.get("bytes") if isinstance(answer, dict) else None
|
|
262
|
+
if not resumed and isinstance(have, int) and 0 <= have <= sent and answer.get("error") == "offset":
|
|
263
|
+
# A piece got lost on the way (the file isn't in Photos yet: nothing to undo). Carry on once
|
|
264
|
+
# from where the runner's copy stands.
|
|
265
|
+
resumed, sent = True, have
|
|
266
|
+
continue
|
|
267
|
+
raise PhoneIOError(f"{file.name} didn't reach {name} whole.", "failed",
|
|
268
|
+
"Keep the iPhone unlocked and connected, then try again.")
|
|
269
|
+
sent += len(block)
|
|
270
|
+
if progress is not None:
|
|
271
|
+
progress(sent, size)
|
|
272
|
+
body = {"upload": upload, "kind": kind, "extension": ext, "filename": file.name[:120], "sha256": checksum,
|
|
273
|
+
"authorizationTimeout": AUTHORIZATION_WAIT_S}
|
|
274
|
+
if album:
|
|
275
|
+
body["album"] = album
|
|
276
|
+
if date_now:
|
|
277
|
+
body["dateNow"] = True
|
|
278
|
+
try:
|
|
279
|
+
return _runner_answer(request, "POST", "/mobster/photos/save", body,
|
|
280
|
+
AUTHORIZATION_WAIT_S + 120 + size / 20_000_000, name=name)
|
|
281
|
+
except PhoneIOError as error:
|
|
282
|
+
if error.code == "failed":
|
|
283
|
+
raise _lost(file, name) from None
|
|
284
|
+
raise
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def recent(request, count=1, since=None, name="the iPhone"):
|
|
288
|
+
"""The newest photos and videos in the phone's library, newest first."""
|
|
289
|
+
body = {"limit": max(1, min(int(count or 1), MAX_RECENT))}
|
|
290
|
+
if since is not None:
|
|
291
|
+
body["since"] = float(since)
|
|
292
|
+
value = _runner_answer(request, "POST", "/mobster/photos/recent", body, 30, name=name)
|
|
293
|
+
if not isinstance(value, dict):
|
|
294
|
+
raise PhoneIOError(NEEDS_REBUILD.format(name=name), "needs_rebuild")
|
|
295
|
+
if value.get("status") not in ("authorized", "limited"):
|
|
296
|
+
raise _refused({"status": value.get("status")}, Path("the library"), name)
|
|
297
|
+
items = []
|
|
298
|
+
for item in value.get("items") or ():
|
|
299
|
+
if isinstance(item, dict) and isinstance(item.get("id"), str) and item["id"]:
|
|
300
|
+
items.append({"id": item["id"][:200], "filename": str(item.get("filename") or "")[:200],
|
|
301
|
+
"kind": "video" if item.get("kind") == "video" else "photo",
|
|
302
|
+
"created": float(item.get("created") or 0), "width": int(item.get("width") or 0),
|
|
303
|
+
"height": int(item.get("height") or 0), "duration": float(item.get("duration") or 0)})
|
|
304
|
+
return items
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def checksum(request, item_id, name="the iPhone"):
|
|
308
|
+
try:
|
|
309
|
+
value = _runner_answer(request, "POST", "/mobster/photos/export", {"id": item_id, "offset": 0, "length": 0},
|
|
310
|
+
900, name=name)
|
|
311
|
+
except PhoneIOError as error:
|
|
312
|
+
if error.code == "needs_rebuild":
|
|
313
|
+
return None
|
|
314
|
+
raise
|
|
315
|
+
if not isinstance(value, dict) or value.get("error") or not value.get("sha256"):
|
|
316
|
+
return None
|
|
317
|
+
return {"bytes": int(value.get("bytes") or 0), "sha256": str(value["sha256"]),
|
|
318
|
+
"filename": str(value.get("filename") or "")}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def export(request, item, folder, *, progress=None, chunk=CHUNK_BYTES, name="the iPhone"):
|
|
322
|
+
"""Copy one library item into ``folder`` over the runner, in pieces, checked against the phone's SHA-256."""
|
|
323
|
+
from .files import land
|
|
324
|
+
folder = Path(folder).expanduser()
|
|
325
|
+
folder.mkdir(parents=True, exist_ok=True)
|
|
326
|
+
offset, total, expected, filename = 0, None, None, item.get("filename") or ""
|
|
327
|
+
hashed = hashlib.sha256()
|
|
328
|
+
with tempfile.TemporaryDirectory(prefix="mobster-pull-", dir=folder) as scratch:
|
|
329
|
+
partial = Path(scratch) / "file"
|
|
330
|
+
with open(partial, "wb") as out:
|
|
331
|
+
while True:
|
|
332
|
+
value = _runner_answer(request, "POST", "/mobster/photos/export",
|
|
333
|
+
{"id": item["id"], "offset": offset, "length": chunk},
|
|
334
|
+
900 if offset == 0 else 120, name=name)
|
|
335
|
+
if not isinstance(value, dict):
|
|
336
|
+
raise PhoneIOError(NEEDS_REBUILD.format(name=name), "needs_rebuild")
|
|
337
|
+
if value.get("error"):
|
|
338
|
+
if value["error"] == "not_found":
|
|
339
|
+
raise PhoneIOError(f"That item isn't in Photos on {name} any more.", "not_found")
|
|
340
|
+
if value["error"] == "status":
|
|
341
|
+
raise _refused({"status": value.get("status")}, Path("the library"), name)
|
|
342
|
+
raise PhoneIOError(f"{name} couldn't read that item ({value['error']}).", "failed",
|
|
343
|
+
"Keep the iPhone unlocked and connected, then try again.")
|
|
344
|
+
if total is None:
|
|
345
|
+
total, expected = int(value.get("bytes") or 0), str(value.get("sha256") or "")
|
|
346
|
+
filename = filename or str(value.get("filename") or "")
|
|
347
|
+
if total > MAX_CHUNKED_BYTES:
|
|
348
|
+
raise PhoneIOError("That item is over 4 GB.", "usage")
|
|
349
|
+
if int(value.get("offset") or 0) != offset or value.get("sha256") != expected:
|
|
350
|
+
raise PhoneIOError(f"The copy from {name} went out of step.", "failed", "Try again.")
|
|
351
|
+
data = base64.b64decode(value.get("data") or "")
|
|
352
|
+
out.write(data)
|
|
353
|
+
hashed.update(data)
|
|
354
|
+
offset += len(data)
|
|
355
|
+
if progress is not None:
|
|
356
|
+
progress(offset, total)
|
|
357
|
+
if value.get("done") or not data:
|
|
358
|
+
break
|
|
359
|
+
if offset != total or hashed.hexdigest() != expected:
|
|
360
|
+
raise PhoneIOError(f"{filename or 'That item'} didn't arrive whole from {name}: its checksum differs.",
|
|
361
|
+
"checksum", "Try again.")
|
|
362
|
+
from ..attachments.sniff import clean_name
|
|
363
|
+
wanted = clean_name(filename or f"IMG_{int(item.get('created') or 0)}")
|
|
364
|
+
target = land(partial, folder, wanted)
|
|
365
|
+
created = float(item.get("created") or 0)
|
|
366
|
+
if created > 0:
|
|
367
|
+
try:
|
|
368
|
+
os.utime(target, (created, created))
|
|
369
|
+
except OSError:
|
|
370
|
+
pass
|
|
371
|
+
return {"path": str(target), "name": target.name, "bytes": total, "kind": item.get("kind") or "photo",
|
|
372
|
+
"sha256": expected, "verified": True, "modified": created or None}
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
class CameraRoll:
|
|
376
|
+
"""The camera roll's files over the cable (AFC's media root)."""
|
|
377
|
+
|
|
378
|
+
def __init__(self, files):
|
|
379
|
+
self.files = files
|
|
380
|
+
|
|
381
|
+
def _afc(self, *command, timeout=60):
|
|
382
|
+
from .files import ANSI, _explain
|
|
383
|
+
argv = [self.files._afcclient(), "-u", self.files.udid, "--", *map(str, command)]
|
|
384
|
+
code, output = self.files.run(argv, timeout)
|
|
385
|
+
output = ANSI.sub("", output)
|
|
386
|
+
if code != 0:
|
|
387
|
+
raise _explain(output, self.files.name)
|
|
388
|
+
errors = [line for line in output.splitlines() if line.startswith(("Error:", "ERROR:"))]
|
|
389
|
+
return output, errors
|
|
390
|
+
|
|
391
|
+
def _ls(self, folder):
|
|
392
|
+
from ..attachments.sniff import clean_name
|
|
393
|
+
from .files import LINE
|
|
394
|
+
output, errors = self._afc("ls", "-l", folder, timeout=30)
|
|
395
|
+
if errors:
|
|
396
|
+
from .files import _explain
|
|
397
|
+
raise _explain("\n".join(errors), self.files.name)
|
|
398
|
+
entries = []
|
|
399
|
+
for line in output.splitlines():
|
|
400
|
+
found = LINE.match(line.rstrip("\n"))
|
|
401
|
+
if not found:
|
|
402
|
+
continue
|
|
403
|
+
kind, size, stamp, name = found.groups()
|
|
404
|
+
if clean_name(name) != name:
|
|
405
|
+
continue # A device's listing cannot name a path outside the destination folder.
|
|
406
|
+
try:
|
|
407
|
+
modified = time.mktime(time.strptime(stamp, "%d %b %Y %H:%M:%S"))
|
|
408
|
+
except (ValueError, OverflowError):
|
|
409
|
+
modified = 0.0
|
|
410
|
+
entries.append({"name": name, "bytes": int(size), "modified": modified, "folder": kind == "d"})
|
|
411
|
+
return entries
|
|
412
|
+
|
|
413
|
+
def media(self):
|
|
414
|
+
"""Every photo and video file in the camera roll, for a before and after count (AirDrop keeps file times)."""
|
|
415
|
+
found = {}
|
|
416
|
+
for folder in (e["name"] for e in self._ls(MEDIA_ROOT) if e["folder"] and DCIM_FOLDER.match(e["name"])):
|
|
417
|
+
for entry in self._ls(f"{MEDIA_ROOT}/{folder}"):
|
|
418
|
+
if not entry["folder"] and PULLED.search(entry["name"]):
|
|
419
|
+
found[f"{MEDIA_ROOT}/{folder}/{entry['name']}"] = entry["bytes"]
|
|
420
|
+
return found
|
|
421
|
+
|
|
422
|
+
def fetch(self, remote, folder, size=None):
|
|
423
|
+
"""Copy one camera roll file (a ``media`` path) into ``folder``; its local Path. Checked by size."""
|
|
424
|
+
from .files import _explain, land
|
|
425
|
+
folder = Path(folder).expanduser()
|
|
426
|
+
folder.mkdir(parents=True, exist_ok=True)
|
|
427
|
+
name = remote.rsplit("/", 1)[-1]
|
|
428
|
+
with tempfile.TemporaryDirectory(prefix="mobster-pull-", dir=folder) as scratch:
|
|
429
|
+
partial = Path(scratch) / "file"
|
|
430
|
+
_, errors = self._afc("get", remote, str(partial), timeout=120 + (size or 0) / 5_000_000)
|
|
431
|
+
if errors or not partial.is_file():
|
|
432
|
+
raise _explain("\n".join(errors) or "Error: nothing arrived", self.files.name)
|
|
433
|
+
if size is not None and partial.stat().st_size != size:
|
|
434
|
+
raise PhoneIOError(f"{name} didn't arrive whole.", "failed", "Try again.")
|
|
435
|
+
return land(partial, folder, name)
|
|
436
|
+
|
|
437
|
+
def newest(self, count=1, since=None):
|
|
438
|
+
count = max(1, min(int(count or 1), MAX_PULL))
|
|
439
|
+
folders = sorted((e["name"] for e in self._ls(MEDIA_ROOT) if e["folder"] and DCIM_FOLDER.match(e["name"])),
|
|
440
|
+
reverse=True)
|
|
441
|
+
found = []
|
|
442
|
+
for folder in folders:
|
|
443
|
+
entries = [e for e in self._ls(f"{MEDIA_ROOT}/{folder}") if not e["folder"] and PULLED.search(e["name"])]
|
|
444
|
+
for entry in entries:
|
|
445
|
+
if since is not None and entry["modified"] < since:
|
|
446
|
+
continue
|
|
447
|
+
ext = entry["name"].rsplit(".", 1)[-1].lower()
|
|
448
|
+
found.append({"path": f"{MEDIA_ROOT}/{folder}/{entry['name']}", "name": entry["name"],
|
|
449
|
+
"bytes": entry["bytes"], "modified": entry["modified"],
|
|
450
|
+
"kind": PHOTO_TYPES.get(ext, "photo")})
|
|
451
|
+
found.sort(key=lambda item: (item["modified"], item["name"]), reverse=True)
|
|
452
|
+
return found[:count]
|
|
453
|
+
|
|
454
|
+
def pull(self, folder, count=1, since=None, convert_heic=False, sips=None):
|
|
455
|
+
"""Copy the newest media into the Mac folder ``folder``; a name already there gets a new one."""
|
|
456
|
+
from .files import land
|
|
457
|
+
items = self.newest(count, since)
|
|
458
|
+
if sum(item["bytes"] for item in items) > MAX_PULL_BYTES:
|
|
459
|
+
raise PhoneIOError("Those files are over 500 MB together. Ask for fewer.", "usage")
|
|
460
|
+
folder = Path(folder).expanduser()
|
|
461
|
+
folder.mkdir(parents=True, exist_ok=True)
|
|
462
|
+
out = []
|
|
463
|
+
with tempfile.TemporaryDirectory(prefix="mobster-pull-", dir=folder) as scratch:
|
|
464
|
+
for item in items:
|
|
465
|
+
partial = self.fetch(item["path"], scratch, item["bytes"])
|
|
466
|
+
name = item["name"]
|
|
467
|
+
if convert_heic and name.lower().endswith((".heic", ".heif")):
|
|
468
|
+
converted = _to_jpeg(partial, sips=sips)
|
|
469
|
+
if converted is not None:
|
|
470
|
+
partial, name = converted, name.rsplit(".", 1)[0] + ".jpg"
|
|
471
|
+
target = land(partial, folder, name)
|
|
472
|
+
try:
|
|
473
|
+
os.utime(target, (item["modified"], item["modified"]))
|
|
474
|
+
except OSError:
|
|
475
|
+
pass
|
|
476
|
+
out.append({"path": str(target), "name": target.name, "bytes": target.stat().st_size,
|
|
477
|
+
"kind": item["kind"], "modified": item["modified"],
|
|
478
|
+
"sha256": digest(target)})
|
|
479
|
+
return out
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _to_jpeg(path, sips=None):
|
|
483
|
+
"""A JPEG next to ``path`` made with macOS's sips, or None when sips isn't there or fails."""
|
|
484
|
+
tool = sips or shutil.which("sips") or "/usr/bin/sips"
|
|
485
|
+
target = Path(str(path) + ".jpg")
|
|
486
|
+
try:
|
|
487
|
+
done = subprocess.run([tool, "-s", "format", "jpeg", str(path), "--out", str(target)], capture_output=True,
|
|
488
|
+
timeout=60, stdin=subprocess.DEVNULL)
|
|
489
|
+
except (OSError, subprocess.SubprocessError):
|
|
490
|
+
return None
|
|
491
|
+
return target if done.returncode == 0 and target.is_file() else None
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def parse_since(value, now=None):
|
|
495
|
+
"""Epoch seconds from "10m", "2h", "1d", an ISO time or epoch seconds; None for None. ValueError otherwise."""
|
|
496
|
+
if value is None or value == "":
|
|
497
|
+
return None
|
|
498
|
+
now = time.time() if now is None else now
|
|
499
|
+
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
500
|
+
return float(value)
|
|
501
|
+
text = str(value).strip()
|
|
502
|
+
found = re.fullmatch(r"(\d+(?:\.\d+)?)\s*([smhd])", text, re.I)
|
|
503
|
+
if found:
|
|
504
|
+
return now - float(found.group(1)) * {"s": 1, "m": 60, "h": 3600, "d": 86400}[found.group(2).lower()]
|
|
505
|
+
import datetime
|
|
506
|
+
try:
|
|
507
|
+
stamp = datetime.datetime.fromisoformat(text.replace("Z", "+00:00"))
|
|
508
|
+
except ValueError:
|
|
509
|
+
raise ValueError("since takes 10m, 2h, 1d or an ISO time such as 2026-10-09T14:00") from None
|
|
510
|
+
if stamp.tzinfo is None:
|
|
511
|
+
stamp = stamp.astimezone()
|
|
512
|
+
return stamp.timestamp()
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Bounded USB screen recordings from the iPhone's QuickTime capture device, without WDA."""
|
|
2
|
+
|
|
3
|
+
import fcntl
|
|
4
|
+
import hashlib
|
|
5
|
+
import math
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import subprocess
|
|
9
|
+
import tempfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from .. import native_helpers
|
|
13
|
+
from ..env_file import open_private, private_dir
|
|
14
|
+
from ..listen import ListenError, _answer, capture_hints, helper_path
|
|
15
|
+
from . import PhoneIOError, capture_cancellation
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def record_video(udid: str, *, seconds: float = 10, output: str | Path) -> dict:
|
|
19
|
+
"""Save a completed MOV without overwriting anything; errors and cancellation leave no partial output."""
|
|
20
|
+
if not isinstance(udid, str) or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9-]*", udid):
|
|
21
|
+
raise PhoneIOError("Name the USB iPhone to record with --device.", "usage")
|
|
22
|
+
if (isinstance(seconds, bool) or not isinstance(seconds, (float, int))
|
|
23
|
+
or not 1 <= seconds <= 300):
|
|
24
|
+
raise PhoneIOError("Use a recording duration from 1 to 300 seconds.", "usage")
|
|
25
|
+
if not native_helpers.devices_allowed():
|
|
26
|
+
raise PhoneIOError("Devices are off in this process (MOBSTER_NO_DEVICES=1).", "devices_off")
|
|
27
|
+
target = Path(output).expanduser().absolute()
|
|
28
|
+
if target.suffix.lower() != ".mov":
|
|
29
|
+
raise PhoneIOError("Use an --output path ending in .mov.", "usage")
|
|
30
|
+
if os.path.lexists(target):
|
|
31
|
+
raise PhoneIOError("The recording file already exists. Choose another --output path.", "usage")
|
|
32
|
+
try:
|
|
33
|
+
executable = helper_path()
|
|
34
|
+
except ListenError as error:
|
|
35
|
+
raise PhoneIOError(str(error), "tools") from None
|
|
36
|
+
|
|
37
|
+
from ..paths import user_data_dir
|
|
38
|
+
locks = user_data_dir() / "video-capture"
|
|
39
|
+
key = hashlib.sha256(udid.replace("-", "").upper().encode()).hexdigest()[:24]
|
|
40
|
+
try:
|
|
41
|
+
private_dir(locks)
|
|
42
|
+
with open_private(locks / (key + ".lock"), "a+") as lock:
|
|
43
|
+
try:
|
|
44
|
+
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
45
|
+
except BlockingIOError:
|
|
46
|
+
raise PhoneIOError("Mobster is already recording this iPhone. Stop that recording first.",
|
|
47
|
+
"capture_busy") from None
|
|
48
|
+
return _record(executable, udid, seconds, target, capture_hints(udid), lock.fileno())
|
|
49
|
+
except OSError:
|
|
50
|
+
raise PhoneIOError("Mobster couldn't save the recording. "
|
|
51
|
+
"Check the output folder and its permissions.") from None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _stop(process):
|
|
55
|
+
try:
|
|
56
|
+
if process.poll() is None:
|
|
57
|
+
try:
|
|
58
|
+
process.terminate()
|
|
59
|
+
process.wait(timeout=7)
|
|
60
|
+
except subprocess.TimeoutExpired:
|
|
61
|
+
process.kill()
|
|
62
|
+
process.wait(timeout=5)
|
|
63
|
+
except ProcessLookupError:
|
|
64
|
+
process.wait(timeout=5)
|
|
65
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
66
|
+
raise PhoneIOError("Mobster couldn't stop video capture. Check running phone-audio processes before "
|
|
67
|
+
"recording again.", "capture_failed") from None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _record(executable, udid, seconds, target, hints, lock_fd):
|
|
71
|
+
# Same filesystem permits atomic publication with link(): an existing target always wins.
|
|
72
|
+
with capture_cancellation(), tempfile.TemporaryDirectory(prefix=".mobster-record-", dir=target.parent) as work:
|
|
73
|
+
os.chmod(work, 0o700)
|
|
74
|
+
argv = [str(executable), "record-video", "--udid", udid, "--seconds", str(seconds), "--work", work]
|
|
75
|
+
argv += hints
|
|
76
|
+
process = None
|
|
77
|
+
try:
|
|
78
|
+
with capture_cancellation(defer=True):
|
|
79
|
+
process = subprocess.Popen(argv, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
|
80
|
+
stderr=subprocess.PIPE,
|
|
81
|
+
text=True, start_new_session=True, pass_fds=(lock_fd,))
|
|
82
|
+
try:
|
|
83
|
+
stdout, _ = process.communicate(timeout=seconds + 20)
|
|
84
|
+
except subprocess.TimeoutExpired:
|
|
85
|
+
raise PhoneIOError("The video helper didn't finish in time. Check the USB connection.",
|
|
86
|
+
"capture_failed") from None
|
|
87
|
+
finally:
|
|
88
|
+
if process is not None:
|
|
89
|
+
try:
|
|
90
|
+
_stop(process)
|
|
91
|
+
finally:
|
|
92
|
+
# Popen.__exit__ would wait without a timeout if the bounded stop failed.
|
|
93
|
+
process.stdout.close()
|
|
94
|
+
process.stderr.close()
|
|
95
|
+
if process.returncode == 2:
|
|
96
|
+
raise PhoneIOError("This Mobster native helper doesn't support video recording. Update Mobster and its "
|
|
97
|
+
"native helpers together.", "tools")
|
|
98
|
+
answer = _answer(stdout)
|
|
99
|
+
if not answer.get("ok"):
|
|
100
|
+
raise PhoneIOError(str(answer.get("message") or "The iPhone couldn't record video."),
|
|
101
|
+
str(answer.get("error") or "capture_failed"))
|
|
102
|
+
movie = Path(work) / "video.mov"
|
|
103
|
+
if process.returncode or not movie.is_file() or movie.is_symlink() or movie.stat().st_size == 0:
|
|
104
|
+
raise PhoneIOError("The video helper produced no completed movie.", "capture_failed")
|
|
105
|
+
duration = answer.get("durationSeconds")
|
|
106
|
+
if type(duration) not in (int, float) or not math.isfinite(duration) or duration <= 0:
|
|
107
|
+
raise PhoneIOError("The video helper produced no readable movie duration.", "capture_failed")
|
|
108
|
+
os.chmod(movie, 0o600)
|
|
109
|
+
try:
|
|
110
|
+
os.link(movie, target)
|
|
111
|
+
except FileExistsError:
|
|
112
|
+
raise PhoneIOError("The recording file already exists. Choose another --output path.", "usage") from None
|
|
113
|
+
return {"ok": True, "path": str(target), "seconds": seconds, "bytes": movie.stat().st_size,
|
|
114
|
+
"audio": answer.get("audio") is True, "transport": "usb", "format": "mov",
|
|
115
|
+
"match": answer.get("match", "udid"), "durationSeconds": duration}
|