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,856 @@
|
|
|
1
|
+
"""The source edits Mobster makes to WebDriverAgent; a source they don't fit fails the build rather than run open."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import plistlib
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .. import phone_ready
|
|
11
|
+
from ..devices import LOOPBACK
|
|
12
|
+
from . import gate as wda_gate
|
|
13
|
+
from . import source as wda_source
|
|
14
|
+
|
|
15
|
+
# Rebranded so free/personal teams can sign them (the stock com.facebook ids fail). The runner's id
|
|
16
|
+
# also gets the team's suffix at build time (signing.runner_bundle_id): App IDs are unique across
|
|
17
|
+
# every Apple team, so a free account can't register an id someone else's team already has.
|
|
18
|
+
BUNDLE_REBRAND = {"com.facebook.WebDriverAgentRunner": "app.mobster.wda.runner",
|
|
19
|
+
"com.facebook.WebDriverAgentLib": "app.mobster.wda.lib"}
|
|
20
|
+
# The runner earlier versions installed under the plain id; removed after the first build with the suffix.
|
|
21
|
+
LEGACY_RUNNER = "app.mobster.wda.runner.xctrunner"
|
|
22
|
+
RUNNER_ID = re.compile(
|
|
23
|
+
r"(?<![\w.])(?:com\.facebook\.WebDriverAgentRunner|app\.mobster\.wda\.runner(?:\.[a-z0-9]{10})?)(?=[\s\";])")
|
|
24
|
+
|
|
25
|
+
WDA_LOCAL_HOST = """
|
|
26
|
+
// Mobster: loopback clients only (the USB and Wi-Fi relays). Mobster's own clients always send Host, so a request
|
|
27
|
+
// without one is refused too.
|
|
28
|
+
static BOOL FBMobsterLocalHost(NSString * _Nullable host)
|
|
29
|
+
{
|
|
30
|
+
if (0 == host.length) {
|
|
31
|
+
return NO;
|
|
32
|
+
}
|
|
33
|
+
NSString *name = (NSString * _Nonnull)host.lowercaseString;
|
|
34
|
+
BOOL bracketed = [name hasPrefix:@"["];
|
|
35
|
+
NSRange end = bracketed ? [name rangeOfString:@"]"] : [name rangeOfString:@":"];
|
|
36
|
+
if (NSNotFound != end.location) {
|
|
37
|
+
name = [name substringToIndex:end.location + (bracketed ? 1 : 0)];
|
|
38
|
+
}
|
|
39
|
+
return [name isEqualToString:@"127.0.0.1"] || [name isEqualToString:@"localhost"] || [name isEqualToString:@"[::1]"];
|
|
40
|
+
}
|
|
41
|
+
"""
|
|
42
|
+
WDA_LOCAL_STREAM = """
|
|
43
|
+
static BOOL FBMobsterLocalStreamRequest(NSData *data)
|
|
44
|
+
{
|
|
45
|
+
NSString *head = [[NSString alloc] initWithData:data encoding:NSISOLatin1StringEncoding].lowercaseString;
|
|
46
|
+
NSString *host = nil;
|
|
47
|
+
for (NSString *line in [head componentsSeparatedByString:@"\\r\\n"]) {
|
|
48
|
+
if ([line hasPrefix:@"origin:"]) {
|
|
49
|
+
return NO;
|
|
50
|
+
}
|
|
51
|
+
if ([line hasPrefix:@"host:"]) {
|
|
52
|
+
host = [[line substringFromIndex:5] stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return FBMobsterLocalHost(host);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
# Mobster's Photos routes (phone_io/photos.py, send_media): save a photo or video the Mac sends into the iPhone's Photos
|
|
60
|
+
# library. The runner's Info.plist (Xcode's XCTRunner template) carries NSPhotoLibraryUsageDescription, not the
|
|
61
|
+
# add-only key, so the read-write level is asked for; "Limit Access" is enough to add (Photos puts what an app adds in
|
|
62
|
+
# its selection). The first save asks on the iPhone; GET /mobster/photos/status says where that stands.
|
|
63
|
+
WDA_PHOTOS_ROUTES = """\
|
|
64
|
+
// Mobster: save media into Photos (send_media).
|
|
65
|
+
[[FBRoute GET:@"/mobster/photos/status"] respondWithTarget:self action:@selector(handleMobsterPhotosStatus:)],
|
|
66
|
+
[[FBRoute GET:@"/mobster/photos/status"].withoutSession respondWithTarget:self action:@selector(handleMobsterPhotosStatus:)],
|
|
67
|
+
[[FBRoute POST:@"/mobster/photos"] respondWithTarget:self action:@selector(handleMobsterPhotosAdd:)],
|
|
68
|
+
[[FBRoute POST:@"/mobster/photos"].withoutSession respondWithTarget:self action:@selector(handleMobsterPhotosAdd:)],
|
|
69
|
+
""" # noqa: E501
|
|
70
|
+
WDA_PHOTOS_COMMANDS = """
|
|
71
|
+
// Mobster: Photos (send_media). One photo or video, sent as base64, saved into the library as a new item.
|
|
72
|
+
static NSString *FBMobsterPhotosStatus(PHAuthorizationStatus status)
|
|
73
|
+
{
|
|
74
|
+
switch (status) {
|
|
75
|
+
case PHAuthorizationStatusAuthorized: return @"authorized";
|
|
76
|
+
case PHAuthorizationStatusLimited: return @"limited";
|
|
77
|
+
case PHAuthorizationStatusDenied: return @"denied";
|
|
78
|
+
case PHAuthorizationStatusRestricted: return @"restricted";
|
|
79
|
+
case PHAuthorizationStatusNotDetermined: return @"not_determined";
|
|
80
|
+
}
|
|
81
|
+
return @"not_determined";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
+ (id<FBResponsePayload>)handleMobsterPhotosStatus:(FBRouteRequest *)request
|
|
85
|
+
{
|
|
86
|
+
PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite];
|
|
87
|
+
return FBResponseWithObject(@{@"status": FBMobsterPhotosStatus(status),
|
|
88
|
+
@"bundleId": NSBundle.mainBundle.bundleIdentifier ?: @""});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
+ (id<FBResponsePayload>)handleMobsterPhotosAdd:(FBRouteRequest *)request
|
|
92
|
+
{
|
|
93
|
+
id encoded = request.arguments[@"data"];
|
|
94
|
+
id kind = request.arguments[@"kind"];
|
|
95
|
+
id extension = request.arguments[@"extension"];
|
|
96
|
+
id filename = request.arguments[@"filename"];
|
|
97
|
+
NSNumber *wait = [request.arguments[@"authorizationTimeout"] isKindOfClass:NSNumber.class]
|
|
98
|
+
? request.arguments[@"authorizationTimeout"] : @60;
|
|
99
|
+
NSRegularExpression *plain = [NSRegularExpression regularExpressionWithPattern:@"^[A-Za-z0-9]{1,5}$" options:(NSRegularExpressionOptions)0 error:nil];
|
|
100
|
+
if (![encoded isKindOfClass:NSString.class] || 0 == [(NSString *)encoded length]
|
|
101
|
+
|| ![extension isKindOfClass:NSString.class]
|
|
102
|
+
|| nil == [plain firstMatchInString:extension options:(NSMatchingOptions)0 range:NSMakeRange(0, [(NSString *)extension length])]
|
|
103
|
+
|| !([kind isEqual:@"photo"] || [kind isEqual:@"video"])) {
|
|
104
|
+
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"data, kind (photo or video) and extension are required" traceback:nil]);
|
|
105
|
+
}
|
|
106
|
+
NSData *data = [[NSData alloc] initWithBase64EncodedString:encoded options:NSDataBase64DecodingIgnoreUnknownCharacters];
|
|
107
|
+
if (nil == data || 0 == data.length) {
|
|
108
|
+
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"data is not base64" traceback:nil]);
|
|
109
|
+
}
|
|
110
|
+
PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite];
|
|
111
|
+
if (PHAuthorizationStatusNotDetermined == status) {
|
|
112
|
+
__block PHAuthorizationStatus answered = PHAuthorizationStatusNotDetermined;
|
|
113
|
+
__block BOOL asked = NO;
|
|
114
|
+
[PHPhotoLibrary requestAuthorizationForAccessLevel:PHAccessLevelReadWrite handler:^(PHAuthorizationStatus value) {
|
|
115
|
+
answered = value;
|
|
116
|
+
asked = YES;
|
|
117
|
+
}];
|
|
118
|
+
[[[[FBRunLoopSpinner new] timeout:MAX(1.0, MIN(wait.doubleValue, 120.0))] interval:0.2] spinUntilTrue:^BOOL{
|
|
119
|
+
return asked;
|
|
120
|
+
}];
|
|
121
|
+
status = asked ? answered : PHAuthorizationStatusNotDetermined;
|
|
122
|
+
}
|
|
123
|
+
if (PHAuthorizationStatusAuthorized != status && PHAuthorizationStatusLimited != status) {
|
|
124
|
+
return FBResponseWithObject(@{@"saved": @NO, @"status": FBMobsterPhotosStatus(status)});
|
|
125
|
+
}
|
|
126
|
+
NSString *name = [NSString stringWithFormat:@"mobster-%@.%@", NSUUID.UUID.UUIDString, extension];
|
|
127
|
+
NSURL *file = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:name]];
|
|
128
|
+
NSError *error;
|
|
129
|
+
if (![data writeToURL:file options:NSDataWritingAtomic error:&error]) {
|
|
130
|
+
return FBResponseWithUnknownError(error);
|
|
131
|
+
}
|
|
132
|
+
__block NSString *identifier = nil;
|
|
133
|
+
__block BOOL finished = NO;
|
|
134
|
+
__block BOOL saved = NO;
|
|
135
|
+
__block NSError *failure = nil;
|
|
136
|
+
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
|
|
137
|
+
PHAssetCreationRequest *creation = [PHAssetCreationRequest creationRequestForAsset];
|
|
138
|
+
PHAssetResourceCreationOptions *options = [PHAssetResourceCreationOptions new];
|
|
139
|
+
options.shouldMoveFile = YES;
|
|
140
|
+
if ([filename isKindOfClass:NSString.class] && [(NSString *)filename length] > 0) {
|
|
141
|
+
options.originalFilename = filename;
|
|
142
|
+
}
|
|
143
|
+
[creation addResourceWithType:([kind isEqual:@"video"] ? PHAssetResourceTypeVideo : PHAssetResourceTypePhoto)
|
|
144
|
+
fileURL:file
|
|
145
|
+
options:options];
|
|
146
|
+
identifier = creation.placeholderForCreatedAsset.localIdentifier;
|
|
147
|
+
} completionHandler:^(BOOL success, NSError *changeError) {
|
|
148
|
+
saved = success;
|
|
149
|
+
failure = changeError;
|
|
150
|
+
finished = YES;
|
|
151
|
+
}];
|
|
152
|
+
[[[[FBRunLoopSpinner new] timeout:120] interval:0.1] spinUntilTrue:^BOOL{
|
|
153
|
+
return finished;
|
|
154
|
+
}];
|
|
155
|
+
[NSFileManager.defaultManager removeItemAtURL:file error:nil];
|
|
156
|
+
if (!saved) {
|
|
157
|
+
return FBResponseWithObject(@{@"saved": @NO, @"status": FBMobsterPhotosStatus(status),
|
|
158
|
+
@"error": failure.localizedDescription ?: (finished ? @"Photos refused the file" : @"Photos didn't answer")});
|
|
159
|
+
}
|
|
160
|
+
return FBResponseWithObject(@{@"saved": @YES, @"status": FBMobsterPhotosStatus(status),
|
|
161
|
+
@"localIdentifier": identifier ?: @""});
|
|
162
|
+
}
|
|
163
|
+
""" # noqa: E501
|
|
164
|
+
# Mobster's transfer routes (phone_io/photos.py, transfer.py): a file in pieces, checked by SHA-256 on the phone before
|
|
165
|
+
# Photos sees it, saved into an album; how many photos and videos the library holds; the newest items listed; and an
|
|
166
|
+
# item's original read back to the Mac in pieces with its SHA-256. A separate patch anchored on the Photos patch's own
|
|
167
|
+
# lines, so a clone already patched for Photos takes it without a fresh checkout. A runner without these answers 404
|
|
168
|
+
# and photos.py falls back to POST /mobster/photos (one piece, at most 100 MB, no album).
|
|
169
|
+
WDA_TRANSFER_ROUTE_ANCHOR = " // Mobster: save media into Photos (send_media).\n"
|
|
170
|
+
WDA_TRANSFER_ROUTES = """\
|
|
171
|
+
// Mobster: transfers (phone_io/transfer.py).
|
|
172
|
+
[[FBRoute GET:@"/mobster/photos/count"] respondWithTarget:self action:@selector(handleMobsterPhotosCount:)],
|
|
173
|
+
[[FBRoute GET:@"/mobster/photos/count"].withoutSession respondWithTarget:self action:@selector(handleMobsterPhotosCount:)],
|
|
174
|
+
[[FBRoute POST:@"/mobster/photos/chunk"] respondWithTarget:self action:@selector(handleMobsterPhotosChunk:)],
|
|
175
|
+
[[FBRoute POST:@"/mobster/photos/chunk"].withoutSession respondWithTarget:self action:@selector(handleMobsterPhotosChunk:)],
|
|
176
|
+
[[FBRoute POST:@"/mobster/photos/save"] respondWithTarget:self action:@selector(handleMobsterPhotosSave:)],
|
|
177
|
+
[[FBRoute POST:@"/mobster/photos/save"].withoutSession respondWithTarget:self action:@selector(handleMobsterPhotosSave:)],
|
|
178
|
+
[[FBRoute POST:@"/mobster/photos/recent"] respondWithTarget:self action:@selector(handleMobsterPhotosRecent:)],
|
|
179
|
+
[[FBRoute POST:@"/mobster/photos/recent"].withoutSession respondWithTarget:self action:@selector(handleMobsterPhotosRecent:)],
|
|
180
|
+
[[FBRoute POST:@"/mobster/photos/export"] respondWithTarget:self action:@selector(handleMobsterPhotosExport:)],
|
|
181
|
+
[[FBRoute POST:@"/mobster/photos/export"].withoutSession respondWithTarget:self action:@selector(handleMobsterPhotosExport:)],
|
|
182
|
+
""" # noqa: E501
|
|
183
|
+
WDA_TRANSFER_IMPORT_ANCHOR = "#import <Photos/Photos.h>\n"
|
|
184
|
+
WDA_TRANSFER_IMPORT = "#import <Photos/Photos.h>\n#import <CommonCrypto/CommonDigest.h>\n"
|
|
185
|
+
# The Photos commands' last lines: the transfer commands follow them, so FBMobsterPhotosStatus is already declared.
|
|
186
|
+
WDA_TRANSFER_COMMAND_ANCHOR = WDA_PHOTOS_COMMANDS[WDA_PHOTOS_COMMANDS.rindex(
|
|
187
|
+
" return FBResponseWithObject(@{@\"saved\": @YES"):]
|
|
188
|
+
WDA_TRANSFER_COMMANDS = """
|
|
189
|
+
// Mobster: transfers (phone_io/transfer.py). Pieces of at most a few MB go to a file in the runner's temporary folder;
|
|
190
|
+
// the save checks the whole file's SHA-256 against the Mac's before Photos sees it.
|
|
191
|
+
static NSString *FBMobsterTransferFolder(void)
|
|
192
|
+
{
|
|
193
|
+
NSString *folder = [NSTemporaryDirectory() stringByAppendingPathComponent:@"mobster-transfer"];
|
|
194
|
+
[NSFileManager.defaultManager createDirectoryAtPath:folder withIntermediateDirectories:YES attributes:nil error:nil];
|
|
195
|
+
return folder;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
static BOOL FBMobsterPlain(id value, NSString *pattern)
|
|
199
|
+
{
|
|
200
|
+
if (![value isKindOfClass:NSString.class] || 0 == [(NSString *)value length]) {
|
|
201
|
+
return NO;
|
|
202
|
+
}
|
|
203
|
+
NSRegularExpression *plain = [NSRegularExpression regularExpressionWithPattern:pattern options:(NSRegularExpressionOptions)0 error:nil];
|
|
204
|
+
return nil != [plain firstMatchInString:value options:(NSMatchingOptions)0 range:NSMakeRange(0, [(NSString *)value length])];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
static NSString *FBMobsterSHA256(NSString *path)
|
|
208
|
+
{
|
|
209
|
+
NSInputStream *stream = [NSInputStream inputStreamWithFileAtPath:path];
|
|
210
|
+
if (nil == stream) {
|
|
211
|
+
return nil;
|
|
212
|
+
}
|
|
213
|
+
[stream open];
|
|
214
|
+
CC_SHA256_CTX context;
|
|
215
|
+
CC_SHA256_Init(&context);
|
|
216
|
+
NSMutableData *buffer = [NSMutableData dataWithLength:(1 << 20)];
|
|
217
|
+
NSInteger count = 0;
|
|
218
|
+
while ((count = [stream read:(uint8_t *)buffer.mutableBytes maxLength:buffer.length]) > 0) {
|
|
219
|
+
CC_SHA256_Update(&context, buffer.bytes, (CC_LONG)count);
|
|
220
|
+
}
|
|
221
|
+
[stream close];
|
|
222
|
+
if (count < 0) {
|
|
223
|
+
return nil;
|
|
224
|
+
}
|
|
225
|
+
unsigned char digest[CC_SHA256_DIGEST_LENGTH];
|
|
226
|
+
CC_SHA256_Final(digest, &context);
|
|
227
|
+
NSMutableString *hex = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH * 2];
|
|
228
|
+
for (int index = 0; index < CC_SHA256_DIGEST_LENGTH; index++) {
|
|
229
|
+
[hex appendFormat:@"%02x", digest[index]];
|
|
230
|
+
}
|
|
231
|
+
return hex;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
static BOOL FBMobsterPhotosAllowed(PHAuthorizationStatus status)
|
|
235
|
+
{
|
|
236
|
+
return PHAuthorizationStatusAuthorized == status || PHAuthorizationStatusLimited == status;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
+ (id<FBResponsePayload>)handleMobsterPhotosCount:(FBRouteRequest *)request
|
|
240
|
+
{
|
|
241
|
+
PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite];
|
|
242
|
+
if (!FBMobsterPhotosAllowed(status)) {
|
|
243
|
+
return FBResponseWithObject(@{@"status": FBMobsterPhotosStatus(status)});
|
|
244
|
+
}
|
|
245
|
+
NSUInteger photos = [PHAsset fetchAssetsWithMediaType:PHAssetMediaTypeImage options:nil].count;
|
|
246
|
+
NSUInteger videos = [PHAsset fetchAssetsWithMediaType:PHAssetMediaTypeVideo options:nil].count;
|
|
247
|
+
return FBResponseWithObject(@{@"status": FBMobsterPhotosStatus(status), @"photos": @(photos), @"videos": @(videos)});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
+ (id<FBResponsePayload>)handleMobsterPhotosChunk:(FBRouteRequest *)request
|
|
251
|
+
{
|
|
252
|
+
id upload = request.arguments[@"upload"];
|
|
253
|
+
id offset = request.arguments[@"offset"];
|
|
254
|
+
id encoded = request.arguments[@"data"];
|
|
255
|
+
if (!FBMobsterPlain(upload, @"^[a-f0-9]{8,32}$") || ![offset isKindOfClass:NSNumber.class]
|
|
256
|
+
|| ![encoded isKindOfClass:NSString.class]) {
|
|
257
|
+
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"upload, offset and data are required" traceback:nil]);
|
|
258
|
+
}
|
|
259
|
+
NSData *data = [[NSData alloc] initWithBase64EncodedString:encoded options:NSDataBase64DecodingIgnoreUnknownCharacters];
|
|
260
|
+
if (nil == data) {
|
|
261
|
+
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"data is not base64" traceback:nil]);
|
|
262
|
+
}
|
|
263
|
+
NSFileManager *files = NSFileManager.defaultManager;
|
|
264
|
+
NSString *path = [FBMobsterTransferFolder() stringByAppendingPathComponent:[@"upload-" stringByAppendingString:upload]];
|
|
265
|
+
unsigned long long start = [(NSNumber *)offset unsignedLongLongValue];
|
|
266
|
+
if (0 == start) {
|
|
267
|
+
[files removeItemAtPath:path error:nil];
|
|
268
|
+
[files createFileAtPath:path contents:nil attributes:nil];
|
|
269
|
+
}
|
|
270
|
+
unsigned long long have = [[files attributesOfItemAtPath:path error:nil] fileSize];
|
|
271
|
+
if (![files fileExistsAtPath:path] || start != have) {
|
|
272
|
+
// Out of step (a piece lost or sent twice): say where the file stands; the Mac starts again from there.
|
|
273
|
+
return FBResponseWithObject(@{@"ok": @NO, @"bytes": @(have), @"error": @"offset"});
|
|
274
|
+
}
|
|
275
|
+
NSFileHandle *handle = [NSFileHandle fileHandleForWritingAtPath:path];
|
|
276
|
+
NSError *error = nil;
|
|
277
|
+
BOOL wrote = nil != handle && [handle seekToEndReturningOffset:nil error:&error] && [handle writeData:data error:&error];
|
|
278
|
+
[handle closeAndReturnError:nil];
|
|
279
|
+
if (!wrote) {
|
|
280
|
+
[files removeItemAtPath:path error:nil];
|
|
281
|
+
return FBResponseWithObject(@{@"ok": @NO, @"bytes": @0, @"error": error.localizedDescription ?: @"write"});
|
|
282
|
+
}
|
|
283
|
+
return FBResponseWithObject(@{@"ok": @YES, @"bytes": @(have + data.length)});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
+ (id<FBResponsePayload>)handleMobsterPhotosSave:(FBRouteRequest *)request
|
|
287
|
+
{
|
|
288
|
+
id upload = request.arguments[@"upload"];
|
|
289
|
+
id kind = request.arguments[@"kind"];
|
|
290
|
+
id filename = request.arguments[@"filename"];
|
|
291
|
+
id album = request.arguments[@"album"];
|
|
292
|
+
id expected = request.arguments[@"sha256"];
|
|
293
|
+
id dateNow = request.arguments[@"dateNow"];
|
|
294
|
+
NSNumber *wait = [request.arguments[@"authorizationTimeout"] isKindOfClass:NSNumber.class]
|
|
295
|
+
? request.arguments[@"authorizationTimeout"] : @60;
|
|
296
|
+
if (!FBMobsterPlain(upload, @"^[a-f0-9]{8,32}$") || !FBMobsterPlain(expected, @"^[a-f0-9]{64}$")
|
|
297
|
+
|| !([kind isEqual:@"photo"] || [kind isEqual:@"video"])) {
|
|
298
|
+
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"upload, sha256 and kind (photo or video) are required" traceback:nil]);
|
|
299
|
+
}
|
|
300
|
+
NSString *path = [FBMobsterTransferFolder() stringByAppendingPathComponent:[@"upload-" stringByAppendingString:upload]];
|
|
301
|
+
NSString *digest = FBMobsterSHA256(path);
|
|
302
|
+
unsigned long long size = [[NSFileManager.defaultManager attributesOfItemAtPath:path error:nil] fileSize];
|
|
303
|
+
if (nil == digest || ![digest isEqualToString:expected]) {
|
|
304
|
+
// What arrived isn't what the Mac sent: Photos never sees it.
|
|
305
|
+
[NSFileManager.defaultManager removeItemAtPath:path error:nil];
|
|
306
|
+
return FBResponseWithObject(@{@"saved": @NO, @"error": @"checksum", @"sha256": digest ?: @"", @"bytes": @(size)});
|
|
307
|
+
}
|
|
308
|
+
PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite];
|
|
309
|
+
if (PHAuthorizationStatusNotDetermined == status) {
|
|
310
|
+
__block PHAuthorizationStatus answered = PHAuthorizationStatusNotDetermined;
|
|
311
|
+
__block BOOL asked = NO;
|
|
312
|
+
[PHPhotoLibrary requestAuthorizationForAccessLevel:PHAccessLevelReadWrite handler:^(PHAuthorizationStatus value) {
|
|
313
|
+
answered = value;
|
|
314
|
+
asked = YES;
|
|
315
|
+
}];
|
|
316
|
+
[[[[FBRunLoopSpinner new] timeout:MAX(1.0, MIN(wait.doubleValue, 120.0))] interval:0.2] spinUntilTrue:^BOOL{
|
|
317
|
+
return asked;
|
|
318
|
+
}];
|
|
319
|
+
status = asked ? answered : PHAuthorizationStatusNotDetermined;
|
|
320
|
+
}
|
|
321
|
+
if (!FBMobsterPhotosAllowed(status)) {
|
|
322
|
+
return FBResponseWithObject(@{@"saved": @NO, @"status": FBMobsterPhotosStatus(status), @"sha256": digest});
|
|
323
|
+
}
|
|
324
|
+
__block NSString *identifier = nil;
|
|
325
|
+
__block BOOL finished = NO;
|
|
326
|
+
__block BOOL saved = NO;
|
|
327
|
+
__block NSError *failure = nil;
|
|
328
|
+
NSURL *file = [NSURL fileURLWithPath:path];
|
|
329
|
+
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
|
|
330
|
+
PHAssetCreationRequest *creation = [PHAssetCreationRequest creationRequestForAsset];
|
|
331
|
+
PHAssetResourceCreationOptions *options = [PHAssetResourceCreationOptions new];
|
|
332
|
+
options.shouldMoveFile = YES;
|
|
333
|
+
if ([filename isKindOfClass:NSString.class] && [(NSString *)filename length] > 0) {
|
|
334
|
+
options.originalFilename = filename;
|
|
335
|
+
}
|
|
336
|
+
[creation addResourceWithType:([kind isEqual:@"video"] ? PHAssetResourceTypeVideo : PHAssetResourceTypePhoto)
|
|
337
|
+
fileURL:file
|
|
338
|
+
options:options];
|
|
339
|
+
if ([dateNow isEqual:@YES]) {
|
|
340
|
+
// Dated now in the library (the file itself is unchanged), so it is first in every app's Recents.
|
|
341
|
+
creation.creationDate = [NSDate date];
|
|
342
|
+
}
|
|
343
|
+
identifier = creation.placeholderForCreatedAsset.localIdentifier;
|
|
344
|
+
} completionHandler:^(BOOL success, NSError *changeError) {
|
|
345
|
+
saved = success;
|
|
346
|
+
failure = changeError;
|
|
347
|
+
finished = YES;
|
|
348
|
+
}];
|
|
349
|
+
[[[[FBRunLoopSpinner new] timeout:600] interval:0.1] spinUntilTrue:^BOOL{
|
|
350
|
+
return finished;
|
|
351
|
+
}];
|
|
352
|
+
[NSFileManager.defaultManager removeItemAtPath:path error:nil];
|
|
353
|
+
if (!saved) {
|
|
354
|
+
return FBResponseWithObject(@{@"saved": @NO, @"status": FBMobsterPhotosStatus(status), @"sha256": digest,
|
|
355
|
+
@"error": failure.localizedDescription ?: (finished ? @"Photos refused the file" : @"Photos didn't answer")});
|
|
356
|
+
}
|
|
357
|
+
// The album is a second change: the item is in Photos whatever the album step says.
|
|
358
|
+
NSString *albumError = @"";
|
|
359
|
+
if ([album isKindOfClass:NSString.class] && [(NSString *)album length] > 0 && identifier.length > 0) {
|
|
360
|
+
PHFetchOptions *named = [PHFetchOptions new];
|
|
361
|
+
named.predicate = [NSPredicate predicateWithFormat:@"title == %@", album];
|
|
362
|
+
PHAssetCollection *collection = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum
|
|
363
|
+
subtype:PHAssetCollectionSubtypeAlbumRegular
|
|
364
|
+
options:named].firstObject;
|
|
365
|
+
PHFetchResult<PHAsset *> *assets = [PHAsset fetchAssetsWithLocalIdentifiers:@[identifier] options:nil];
|
|
366
|
+
__block BOOL filed = NO;
|
|
367
|
+
__block BOOL filedDone = NO;
|
|
368
|
+
__block NSError *fileError = nil;
|
|
369
|
+
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
|
|
370
|
+
PHAssetCollectionChangeRequest *change = nil != collection
|
|
371
|
+
? [PHAssetCollectionChangeRequest changeRequestForAssetCollection:collection]
|
|
372
|
+
: [PHAssetCollectionChangeRequest creationRequestForAssetCollectionWithTitle:album];
|
|
373
|
+
[change addAssets:assets];
|
|
374
|
+
} completionHandler:^(BOOL success, NSError *changeError) {
|
|
375
|
+
filed = success;
|
|
376
|
+
fileError = changeError;
|
|
377
|
+
filedDone = YES;
|
|
378
|
+
}];
|
|
379
|
+
[[[[FBRunLoopSpinner new] timeout:60] interval:0.1] spinUntilTrue:^BOOL{
|
|
380
|
+
return filedDone;
|
|
381
|
+
}];
|
|
382
|
+
if (!filed) {
|
|
383
|
+
albumError = fileError.localizedDescription ?: @"Photos didn't add it to the album";
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return FBResponseWithObject(@{@"saved": @YES, @"status": FBMobsterPhotosStatus(status),
|
|
387
|
+
@"localIdentifier": identifier ?: @"", @"sha256": digest, @"bytes": @(size),
|
|
388
|
+
@"albumError": albumError});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
static PHAssetResource *FBMobsterOriginal(PHAsset *asset)
|
|
392
|
+
{
|
|
393
|
+
NSArray<PHAssetResource *> *resources = [PHAssetResource assetResourcesForAsset:asset];
|
|
394
|
+
PHAssetResourceType wanted = PHAssetMediaTypeVideo == asset.mediaType ? PHAssetResourceTypeVideo : PHAssetResourceTypePhoto;
|
|
395
|
+
for (PHAssetResource *resource in resources) {
|
|
396
|
+
if (resource.type == wanted) {
|
|
397
|
+
return resource;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return resources.firstObject;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
+ (id<FBResponsePayload>)handleMobsterPhotosRecent:(FBRouteRequest *)request
|
|
404
|
+
{
|
|
405
|
+
PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite];
|
|
406
|
+
if (!FBMobsterPhotosAllowed(status)) {
|
|
407
|
+
return FBResponseWithObject(@{@"status": FBMobsterPhotosStatus(status), @"items": @[]});
|
|
408
|
+
}
|
|
409
|
+
NSInteger limit = [request.arguments[@"limit"] isKindOfClass:NSNumber.class] ? [request.arguments[@"limit"] integerValue] : 1;
|
|
410
|
+
PHFetchOptions *options = [PHFetchOptions new];
|
|
411
|
+
options.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]];
|
|
412
|
+
options.fetchLimit = (NSUInteger)MAX(1, MIN(limit, 50));
|
|
413
|
+
NSPredicate *media = [NSPredicate predicateWithFormat:@"mediaType == %d OR mediaType == %d", PHAssetMediaTypeImage, PHAssetMediaTypeVideo];
|
|
414
|
+
id since = request.arguments[@"since"];
|
|
415
|
+
options.predicate = [since isKindOfClass:NSNumber.class]
|
|
416
|
+
? [NSCompoundPredicate andPredicateWithSubpredicates:@[media, [NSPredicate predicateWithFormat:@"creationDate >= %@", [NSDate dateWithTimeIntervalSince1970:[since doubleValue]]]]]
|
|
417
|
+
: media;
|
|
418
|
+
PHFetchResult<PHAsset *> *assets = [PHAsset fetchAssetsWithOptions:options];
|
|
419
|
+
NSMutableArray *items = [NSMutableArray array];
|
|
420
|
+
[assets enumerateObjectsUsingBlock:^(PHAsset *asset, NSUInteger index, BOOL *stop) {
|
|
421
|
+
PHAssetResource *original = FBMobsterOriginal(asset);
|
|
422
|
+
[items addObject:@{@"id": asset.localIdentifier ?: @"",
|
|
423
|
+
@"filename": original.originalFilename ?: @"",
|
|
424
|
+
@"kind": PHAssetMediaTypeVideo == asset.mediaType ? @"video" : @"photo",
|
|
425
|
+
@"created": @(asset.creationDate.timeIntervalSince1970),
|
|
426
|
+
@"width": @(asset.pixelWidth), @"height": @(asset.pixelHeight),
|
|
427
|
+
@"duration": @(asset.duration)}];
|
|
428
|
+
}];
|
|
429
|
+
return FBResponseWithObject(@{@"status": FBMobsterPhotosStatus(status), @"items": items});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
static NSMutableDictionary<NSString *, NSDictionary *> *FBMobsterExports(void)
|
|
433
|
+
{
|
|
434
|
+
static NSMutableDictionary *exports = nil;
|
|
435
|
+
static dispatch_once_t once;
|
|
436
|
+
dispatch_once(&once, ^{
|
|
437
|
+
exports = [NSMutableDictionary dictionary];
|
|
438
|
+
});
|
|
439
|
+
return exports;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
+ (id<FBResponsePayload>)handleMobsterPhotosExport:(FBRouteRequest *)request
|
|
443
|
+
{
|
|
444
|
+
id identifier = request.arguments[@"id"];
|
|
445
|
+
id offset = request.arguments[@"offset"];
|
|
446
|
+
id length = request.arguments[@"length"];
|
|
447
|
+
if (![identifier isKindOfClass:NSString.class] || 0 == [(NSString *)identifier length] || [(NSString *)identifier length] > 200
|
|
448
|
+
|| ![offset isKindOfClass:NSNumber.class] || ![length isKindOfClass:NSNumber.class]) {
|
|
449
|
+
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"id, offset and length are required" traceback:nil]);
|
|
450
|
+
}
|
|
451
|
+
PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite];
|
|
452
|
+
if (!FBMobsterPhotosAllowed(status)) {
|
|
453
|
+
return FBResponseWithObject(@{@"status": FBMobsterPhotosStatus(status), @"error": @"status"});
|
|
454
|
+
}
|
|
455
|
+
NSMutableDictionary *exports = FBMobsterExports();
|
|
456
|
+
NSDictionary *export = exports[identifier];
|
|
457
|
+
if (0 == [(NSNumber *)offset unsignedLongLongValue] || nil == export) {
|
|
458
|
+
if (nil != export) {
|
|
459
|
+
[NSFileManager.defaultManager removeItemAtPath:export[@"path"] error:nil];
|
|
460
|
+
[exports removeObjectForKey:identifier];
|
|
461
|
+
}
|
|
462
|
+
PHAsset *asset = [PHAsset fetchAssetsWithLocalIdentifiers:@[identifier] options:nil].firstObject;
|
|
463
|
+
PHAssetResource *original = nil == asset ? nil : FBMobsterOriginal(asset);
|
|
464
|
+
if (nil == original) {
|
|
465
|
+
return FBResponseWithObject(@{@"status": FBMobsterPhotosStatus(status), @"error": @"not_found"});
|
|
466
|
+
}
|
|
467
|
+
NSString *path = [FBMobsterTransferFolder() stringByAppendingPathComponent:[@"export-" stringByAppendingString:NSUUID.UUID.UUIDString]];
|
|
468
|
+
PHAssetResourceRequestOptions *options = [PHAssetResourceRequestOptions new];
|
|
469
|
+
options.networkAccessAllowed = YES; // an original kept only in iCloud (Optimize iPhone Storage) is fetched first
|
|
470
|
+
__block BOOL written = NO;
|
|
471
|
+
__block BOOL writeDone = NO;
|
|
472
|
+
__block NSError *writeError = nil;
|
|
473
|
+
[[PHAssetResourceManager defaultManager] writeDataForAssetResource:original toFile:[NSURL fileURLWithPath:path]
|
|
474
|
+
options:options completionHandler:^(NSError *error) {
|
|
475
|
+
written = nil == error;
|
|
476
|
+
writeError = error;
|
|
477
|
+
writeDone = YES;
|
|
478
|
+
}];
|
|
479
|
+
[[[[FBRunLoopSpinner new] timeout:900] interval:0.1] spinUntilTrue:^BOOL{
|
|
480
|
+
return writeDone;
|
|
481
|
+
}];
|
|
482
|
+
NSString *digest = written ? FBMobsterSHA256(path) : nil;
|
|
483
|
+
if (nil == digest) {
|
|
484
|
+
[NSFileManager.defaultManager removeItemAtPath:path error:nil];
|
|
485
|
+
return FBResponseWithObject(@{@"status": FBMobsterPhotosStatus(status), @"error": writeError.localizedDescription ?: @"export"});
|
|
486
|
+
}
|
|
487
|
+
unsigned long long size = [[NSFileManager.defaultManager attributesOfItemAtPath:path error:nil] fileSize];
|
|
488
|
+
export = @{@"path": path, @"sha256": digest, @"bytes": @(size), @"filename": original.originalFilename ?: @""};
|
|
489
|
+
exports[identifier] = export;
|
|
490
|
+
}
|
|
491
|
+
unsigned long long start = [(NSNumber *)offset unsignedLongLongValue];
|
|
492
|
+
unsigned long long size = [export[@"bytes"] unsignedLongLongValue];
|
|
493
|
+
// length 0: only the size and the checksum (to check an item that arrived another way), and the copy goes.
|
|
494
|
+
NSUInteger count = (NSUInteger)MIN([(NSNumber *)length unsignedLongLongValue], (unsigned long long)(16 << 20));
|
|
495
|
+
NSString *exportPath = export[@"path"];
|
|
496
|
+
NSData *data = [NSData data];
|
|
497
|
+
if (count > 0) {
|
|
498
|
+
NSFileHandle *handle = [NSFileHandle fileHandleForReadingAtPath:exportPath];
|
|
499
|
+
data = nil;
|
|
500
|
+
if (nil != handle && start <= size && [handle seekToOffset:start error:nil]) {
|
|
501
|
+
data = [handle readDataUpToLength:count error:nil];
|
|
502
|
+
}
|
|
503
|
+
[handle closeAndReturnError:nil];
|
|
504
|
+
}
|
|
505
|
+
if (nil == data) {
|
|
506
|
+
return FBResponseWithObject(@{@"status": FBMobsterPhotosStatus(status), @"error": @"read"});
|
|
507
|
+
}
|
|
508
|
+
BOOL done = 0 == count || start + data.length >= size;
|
|
509
|
+
if (done) {
|
|
510
|
+
[NSFileManager.defaultManager removeItemAtPath:exportPath error:nil];
|
|
511
|
+
[exports removeObjectForKey:identifier];
|
|
512
|
+
}
|
|
513
|
+
return FBResponseWithObject(@{@"status": FBMobsterPhotosStatus(status), @"filename": export[@"filename"],
|
|
514
|
+
@"bytes": export[@"bytes"], @"sha256": export[@"sha256"], @"offset": @(start),
|
|
515
|
+
@"data": [data base64EncodedStringWithOptions:0], @"done": @(done)});
|
|
516
|
+
}
|
|
517
|
+
""" # noqa: E501
|
|
518
|
+
TRANSFER_COMMANDS_PATCH = ("WebDriverAgentLib/Commands/FBCustomCommands.m", WDA_TRANSFER_COMMAND_ANCHOR,
|
|
519
|
+
WDA_TRANSFER_COMMAND_ANCHOR + WDA_TRANSFER_COMMANDS)
|
|
520
|
+
# Two routes for the content queue's checks (phone_ready.py), applied only with MOBSTER_QUEUE_PREFLIGHT=1: without it
|
|
521
|
+
# the patch set, its digest and the runner stay exactly as they were. POST /mobster/photos/authorize asks iOS for Photos
|
|
522
|
+
# access and saves nothing: the person answers on the phone, and Mobster never presses the grant
|
|
523
|
+
# (system_dialogs.GRANTS).
|
|
524
|
+
# It asks only while the status is not_determined, so it never re-asks and never resets an answer. GET
|
|
525
|
+
# /mobster/power says whether Low Power Mode is on (the phone locks itself after 30 seconds), and nothing else. Both
|
|
526
|
+
# are anchored on stock WebDriverAgent lines that no other patch uses, so they apply beside the Photos and transfer
|
|
527
|
+
# routes.
|
|
528
|
+
PREFLIGHT_ROUTE_ANCHOR = (" [[FBRoute GET:@\"/wda/voiceOver/enabled\"].withoutSession respondWithTarget:self "
|
|
529
|
+
"action:@selector(handleVoiceOverEnabled:)],\n")
|
|
530
|
+
PREFLIGHT_ROUTES = """\
|
|
531
|
+
// Mobster: the content queue's checks (phone_ready.py).
|
|
532
|
+
[[FBRoute POST:@"/mobster/photos/authorize"] respondWithTarget:self action:@selector(handleMobsterPhotosAuthorize:)],
|
|
533
|
+
[[FBRoute POST:@"/mobster/photos/authorize"].withoutSession respondWithTarget:self action:@selector(handleMobsterPhotosAuthorize:)],
|
|
534
|
+
[[FBRoute GET:@"/mobster/power"] respondWithTarget:self action:@selector(handleMobsterPower:)],
|
|
535
|
+
[[FBRoute GET:@"/mobster/power"].withoutSession respondWithTarget:self action:@selector(handleMobsterPower:)],
|
|
536
|
+
""" # noqa: E501
|
|
537
|
+
PREFLIGHT_COMMAND_ANCHOR = "+ (id<FBResponsePayload>)handleDismissKeyboardCommand:(FBRouteRequest *)request\n"
|
|
538
|
+
PREFLIGHT_COMMANDS = """\
|
|
539
|
+
// Mobster: ask for Photos access without saving anything (phone_ready.py). Only a status that is still not_determined
|
|
540
|
+
// is asked about, so an answer the person gave is never asked for again and never changed. The person answers on the
|
|
541
|
+
// iPhone; this waits for that answer up to authorizationTimeout seconds and then says where it stands.
|
|
542
|
+
+ (id<FBResponsePayload>)handleMobsterPhotosAuthorize:(FBRouteRequest *)request
|
|
543
|
+
{
|
|
544
|
+
NSNumber *wait = [request.arguments[@"authorizationTimeout"] isKindOfClass:NSNumber.class]
|
|
545
|
+
? request.arguments[@"authorizationTimeout"] : @20;
|
|
546
|
+
PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite];
|
|
547
|
+
BOOL asked = NO;
|
|
548
|
+
if (PHAuthorizationStatusNotDetermined == status) {
|
|
549
|
+
asked = YES;
|
|
550
|
+
__block PHAuthorizationStatus answered = PHAuthorizationStatusNotDetermined;
|
|
551
|
+
__block BOOL finished = NO;
|
|
552
|
+
[PHPhotoLibrary requestAuthorizationForAccessLevel:PHAccessLevelReadWrite handler:^(PHAuthorizationStatus value) {
|
|
553
|
+
answered = value;
|
|
554
|
+
finished = YES;
|
|
555
|
+
}];
|
|
556
|
+
[[[[FBRunLoopSpinner new] timeout:MAX(1.0, MIN(wait.doubleValue, 120.0))] interval:0.2] spinUntilTrue:^BOOL{
|
|
557
|
+
return finished;
|
|
558
|
+
}];
|
|
559
|
+
status = finished ? answered : PHAuthorizationStatusNotDetermined;
|
|
560
|
+
}
|
|
561
|
+
return FBResponseWithObject(@{@"status": FBMobsterPhotosStatus(status), @"asked": @(asked)});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Mobster: whether Low Power Mode is on (phone_ready.py). It reads one process flag and changes nothing.
|
|
565
|
+
+ (id<FBResponsePayload>)handleMobsterPower:(FBRouteRequest *)request
|
|
566
|
+
{
|
|
567
|
+
return FBResponseWithObject(@{@"lowPowerMode": @(NSProcessInfo.processInfo.isLowPowerModeEnabled)});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
""" # noqa: E501
|
|
571
|
+
# The chunked save keeps the upload in a file named upload-<id> with no extension, and PhotoKit refused it with
|
|
572
|
+
# PHPhotosErrorInvalidResource (3302) on an iOS 26.4 simulator under Limit Access (19 Oct 2026), while the one-piece
|
|
573
|
+
# route, whose file has an extension, saved. The Mac sends the extension with the save, so with the flag on the file is
|
|
574
|
+
# renamed to
|
|
575
|
+
# carry it first. It is the transfer patch's own text with those lines added (a variant, not an edit of its output), so
|
|
576
|
+
# every patch still applies to the unpatched file and again, unchanged, to a patched one.
|
|
577
|
+
PREFLIGHT_SAVE_ANCHOR = " NSURL *file = [NSURL fileURLWithPath:path];\n"
|
|
578
|
+
PREFLIGHT_SAVE_NAMED = """\
|
|
579
|
+
id suffix = request.arguments[@"extension"];
|
|
580
|
+
NSString *extension = FBMobsterPlain(suffix, @"^[A-Za-z0-9]{1,5}$") ? suffix : ([kind isEqual:@"video"] ? @"mov" : @"jpg");
|
|
581
|
+
NSString *named = [path stringByAppendingPathExtension:extension];
|
|
582
|
+
[NSFileManager.defaultManager removeItemAtPath:named error:nil];
|
|
583
|
+
if ([NSFileManager.defaultManager moveItemAtPath:path toPath:named error:nil]) {
|
|
584
|
+
path = named;
|
|
585
|
+
}
|
|
586
|
+
""" # noqa: E501
|
|
587
|
+
assert WDA_TRANSFER_COMMANDS.count(PREFLIGHT_SAVE_ANCHOR) == 1
|
|
588
|
+
TRANSFER_COMMANDS_NAMED_PATCH = (
|
|
589
|
+
TRANSFER_COMMANDS_PATCH[0], TRANSFER_COMMANDS_PATCH[1],
|
|
590
|
+
TRANSFER_COMMANDS_PATCH[1] + WDA_TRANSFER_COMMANDS.replace(PREFLIGHT_SAVE_ANCHOR,
|
|
591
|
+
PREFLIGHT_SAVE_NAMED + PREFLIGHT_SAVE_ANCHOR, 1))
|
|
592
|
+
PREFLIGHT_PATCHES = (
|
|
593
|
+
("WebDriverAgentLib/Commands/FBCustomCommands.m", PREFLIGHT_ROUTE_ANCHOR,
|
|
594
|
+
PREFLIGHT_ROUTE_ANCHOR + PREFLIGHT_ROUTES),
|
|
595
|
+
("WebDriverAgentLib/Commands/FBCustomCommands.m", PREFLIGHT_COMMAND_ANCHOR,
|
|
596
|
+
PREFLIGHT_COMMANDS + PREFLIGHT_COMMAND_ANCHOR),
|
|
597
|
+
)
|
|
598
|
+
# The CLI password route (/mobster/secret) never logs its body and types through no unredacted XCTest event. The
|
|
599
|
+
# target must already be focused and empty; validation and the typing stay together on WDA's main queue, and the
|
|
600
|
+
# route never taps, clears, submits, retries or unlocks a phone.
|
|
601
|
+
WDA_SECRET_COMMANDS = r'''
|
|
602
|
+
+ (id<FBResponsePayload>)handleMobsterSecret:(FBRouteRequest *)request
|
|
603
|
+
{
|
|
604
|
+
// Never include request arguments or exception descriptions in this route's response.
|
|
605
|
+
@try {
|
|
606
|
+
NSString *bundle = request.arguments[@"bundleId"];
|
|
607
|
+
NSString *identifier = request.arguments[@"identifier"];
|
|
608
|
+
BOOL checkOnly = [request.URL.path.lowercaseString hasSuffix:@"/check"];
|
|
609
|
+
NSString *text = request.arguments[@"secret"];
|
|
610
|
+
if (![bundle isKindOfClass:NSString.class] || bundle.length == 0 ||
|
|
611
|
+
[bundle isEqualToString:@"com.apple.springboard"] ||
|
|
612
|
+
![identifier isKindOfClass:NSString.class] || identifier.length == 0) {
|
|
613
|
+
return FBResponseWithObject(@{@"ok": @NO, @"code": @"target"});
|
|
614
|
+
}
|
|
615
|
+
if (!checkOnly && (![text isKindOfClass:NSString.class] || text.length == 0 ||
|
|
616
|
+
text.length > 256 || [text rangeOfCharacterFromSet:NSCharacterSet.controlCharacterSet].location != NSNotFound ||
|
|
617
|
+
[text rangeOfCharacterFromSet:NSCharacterSet.newlineCharacterSet].location != NSNotFound)) {
|
|
618
|
+
return FBResponseWithObject(@{@"ok": @NO, @"code": @"secret"});
|
|
619
|
+
}
|
|
620
|
+
XCUIApplication *app = XCUIApplication.fb_activeApplication;
|
|
621
|
+
if (XCUIDevice.sharedDevice.fb_isScreenLocked || ![app.bundleID isEqualToString:bundle]) {
|
|
622
|
+
return FBResponseWithObject(@{@"ok": @NO, @"code": @"foreground"});
|
|
623
|
+
}
|
|
624
|
+
NSArray<XCUIElement *> *matches = [[app.secureTextFields matchingIdentifier:identifier] allElementsBoundByIndex];
|
|
625
|
+
if (matches.count != 1) {
|
|
626
|
+
return FBResponseWithObject(@{@"ok": @NO, @"code": @"target"});
|
|
627
|
+
}
|
|
628
|
+
XCUIElement *field = matches.firstObject;
|
|
629
|
+
if (!field.enabled || !field.hittable) {
|
|
630
|
+
return FBResponseWithObject(@{@"ok": @NO, @"code": @"field"});
|
|
631
|
+
}
|
|
632
|
+
// Resolve focus last. The device owner can still change focus outside WDA's queue.
|
|
633
|
+
id<FBXCElementSnapshot> snapshot = [field fb_standardSnapshot];
|
|
634
|
+
id value = snapshot.value;
|
|
635
|
+
BOOL empty = value == nil || ([value isKindOfClass:NSString.class] &&
|
|
636
|
+
([(NSString *)value length] == 0 || [value isEqual:snapshot.placeholderValue]));
|
|
637
|
+
if (snapshot.elementType != XCUIElementTypeSecureTextField || !snapshot.hasKeyboardFocus || !empty) {
|
|
638
|
+
return FBResponseWithObject(@{@"ok": @NO, @"code": @"field"});
|
|
639
|
+
}
|
|
640
|
+
// Snapshot reads can yield to the app or lock screen; verify the foreground afterward.
|
|
641
|
+
if (XCUIDevice.sharedDevice.fb_isScreenLocked ||
|
|
642
|
+
![XCUIApplication.fb_activeApplication.bundleID isEqualToString:bundle]) {
|
|
643
|
+
return FBResponseWithObject(@{@"ok": @NO, @"code": @"foreground"});
|
|
644
|
+
}
|
|
645
|
+
if (checkOnly) {
|
|
646
|
+
return FBResponseWithObject(@{@"ok": @YES, @"version": @1});
|
|
647
|
+
}
|
|
648
|
+
XCSynthesizedEventRecord *event = [[XCSynthesizedEventRecord alloc] initWithName:@"Mobster private text entry"];
|
|
649
|
+
XCPointerEventPath *keys = [[XCPointerEventPath alloc] initForTextInput];
|
|
650
|
+
// A fixed 60 keys/s keeps even the maximum input below the completion deadline.
|
|
651
|
+
[keys typeText:text atOffset:0.0 typingSpeed:60 shouldRedact:YES];
|
|
652
|
+
[event addPointerEventPath:keys];
|
|
653
|
+
// Upstream's helper ignores the completion BOOL and waits without a deadline.
|
|
654
|
+
// Preserve an uncertain result instead of claiming success or resending input.
|
|
655
|
+
NSObject *state = [NSObject new];
|
|
656
|
+
__block BOOL finished = NO;
|
|
657
|
+
__block BOOL sent = NO;
|
|
658
|
+
[XCUIDevice.sharedDevice.eventSynthesizer synthesizeEvent:event completion:^(BOOL result, NSError *error) {
|
|
659
|
+
@synchronized (state) {
|
|
660
|
+
sent = result && error == nil;
|
|
661
|
+
finished = YES;
|
|
662
|
+
}
|
|
663
|
+
}];
|
|
664
|
+
BOOL completed = [[[FBRunLoopSpinner new] timeout:20] spinUntilTrue:^BOOL {
|
|
665
|
+
@synchronized (state) { return finished; }
|
|
666
|
+
}];
|
|
667
|
+
BOOL confirmed;
|
|
668
|
+
@synchronized (state) { confirmed = completed && sent; }
|
|
669
|
+
return FBResponseWithObject(@{@"ok": @(confirmed), @"code": confirmed ? @"sent" : @"unknown"});
|
|
670
|
+
} @catch (NSException *exception) {
|
|
671
|
+
(void)exception;
|
|
672
|
+
return FBResponseWithObject(@{@"ok": @NO, @"code": @"unknown"});
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
'''
|
|
676
|
+
SECRET_PATCHES = (
|
|
677
|
+
("WebDriverAgentLib/Routing/FBWebServer.m",
|
|
678
|
+
" [FBLogger verboseLog:routeParams.description];",
|
|
679
|
+
" // Mobster private input: never log its body, even with verbose logging enabled.\n"
|
|
680
|
+
" if (![routeParams.URL.path.lowercaseString containsString:@\"/mobster/secret\"]) {\n"
|
|
681
|
+
" [FBLogger verboseLog:routeParams.description];\n"
|
|
682
|
+
" }"),
|
|
683
|
+
("WebDriverAgentLib/Commands/FBCustomCommands.m",
|
|
684
|
+
'#import "FBCustomCommands.h"\n',
|
|
685
|
+
'#import "FBCustomCommands.h"\n'
|
|
686
|
+
'#import "XCUIElement+FBUtilities.h"\n'
|
|
687
|
+
'#import "FBXCElementSnapshot.h"\n'
|
|
688
|
+
'#import "XCUIDevice.h"\n'
|
|
689
|
+
'#import "XCSynthesizedEventRecord.h"\n'
|
|
690
|
+
'#import "XCPointerEventPath.h"\n'),
|
|
691
|
+
("WebDriverAgentLib/Commands/FBCustomCommands.m",
|
|
692
|
+
' [[FBRoute POST:@"/timeouts"] respondWithTarget:self action:@selector(handleTimeouts:)],',
|
|
693
|
+
' [[FBRoute POST:@"/mobster/secret/check"] respondWithTarget:self action:@selector(handleMobsterSecret:)],\n'
|
|
694
|
+
' [[FBRoute POST:@"/mobster/secret"] respondWithTarget:self action:@selector(handleMobsterSecret:)],\n'
|
|
695
|
+
' [[FBRoute POST:@"/timeouts"] respondWithTarget:self action:@selector(handleTimeouts:)],'),
|
|
696
|
+
("WebDriverAgentLib/Commands/FBCustomCommands.m",
|
|
697
|
+
"@implementation FBCustomCommands\n",
|
|
698
|
+
"@implementation FBCustomCommands\n" + WDA_SECRET_COMMANDS),
|
|
699
|
+
)
|
|
700
|
+
# Idempotent (file, anchor, patched) edits: every build applies them to the clone.
|
|
701
|
+
WDA_PATCHES = (
|
|
702
|
+
# The MJPEG socket binds USE_IP like the API; upstream it always listens on every interface.
|
|
703
|
+
("WebDriverAgentLib/Routing/FBWebServer.m",
|
|
704
|
+
"initWithPort:(uint16_t)FBConfiguration.sharedInstance.mjpegServerPort];\n",
|
|
705
|
+
"initWithPort:(uint16_t)FBConfiguration.sharedInstance.mjpegServerPort];\n"
|
|
706
|
+
" // Mobster: the MJPEG socket listens where the API does (USE_IP), not on every interface.\n"
|
|
707
|
+
" self.screenshotsBroadcaster.interface = FBConfiguration.sharedInstance.bindingIPAddress;\n"),
|
|
708
|
+
("WebDriverAgentLib/Routing/FBHTTPServer.m",
|
|
709
|
+
"static const NSUInteger FBMaxRequestHeaderSize = 16 * 1024;\n",
|
|
710
|
+
"static const NSUInteger FBMaxRequestHeaderSize = 16 * 1024;\n" + WDA_LOCAL_HOST),
|
|
711
|
+
("WebDriverAgentLib/Routing/FBHTTPServer.m",
|
|
712
|
+
" NSUInteger contentLength = 0;\n if (![self resolveBodyLength:",
|
|
713
|
+
" if (nil != requestHeaders[@\"origin\"] || !FBMobsterLocalHost(requestHeaders[@\"host\"])) {\n"
|
|
714
|
+
" RouteResponse *forbidden = [RouteResponse new];\n"
|
|
715
|
+
" [FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@\"Local clients only\" "
|
|
716
|
+
"traceback:nil])\n"
|
|
717
|
+
" dispatchWithResponse:forbidden];\n"
|
|
718
|
+
" [self failClient:client withResponse:forbidden];\n"
|
|
719
|
+
" return nil;\n"
|
|
720
|
+
" }\n"
|
|
721
|
+
" NSUInteger contentLength = 0;\n if (![self resolveBodyLength:"),
|
|
722
|
+
("WebDriverAgentLib/Utilities/FBMjpegServer.m",
|
|
723
|
+
"static NSString *const SERVER_NAME = @\"WDA MJPEG Server\";\n",
|
|
724
|
+
"static NSString *const SERVER_NAME = @\"WDA MJPEG Server\";\n" + WDA_LOCAL_HOST + WDA_LOCAL_STREAM),
|
|
725
|
+
("WebDriverAgentLib/Utilities/FBMjpegServer.m",
|
|
726
|
+
" [FBLogger log:@\"Starting screenshots broadcast for the client\"];\n",
|
|
727
|
+
" if (!FBMobsterLocalStreamRequest(data)) {\n"
|
|
728
|
+
" [FBLogger log:@\"Refused a screenshots broadcast request from a browser\"];\n"
|
|
729
|
+
" nw_connection_cancel(client);\n"
|
|
730
|
+
" return;\n"
|
|
731
|
+
" }\n"
|
|
732
|
+
" [FBLogger log:@\"Starting screenshots broadcast for the client\"];\n"),
|
|
733
|
+
# Photos (send_media): the framework, the routes and their commands.
|
|
734
|
+
("WebDriverAgentLib/Commands/FBCustomCommands.m",
|
|
735
|
+
"#import <CoreLocation/CoreLocation.h>\n",
|
|
736
|
+
"#import <CoreLocation/CoreLocation.h>\n#import <Photos/Photos.h>\n"),
|
|
737
|
+
("WebDriverAgentLib/Commands/FBCustomCommands.m",
|
|
738
|
+
" [[FBRoute OPTIONS:@\"/*\"].withoutSession respondWithTarget:self action:@selector(handlePingCommand:)],\n",
|
|
739
|
+
WDA_PHOTOS_ROUTES
|
|
740
|
+
+ " [[FBRoute OPTIONS:@\"/*\"].withoutSession respondWithTarget:self action:@selector(handlePingCommand:)],\n"),
|
|
741
|
+
("WebDriverAgentLib/Commands/FBCustomCommands.m",
|
|
742
|
+
"#pragma mark - Commands\n",
|
|
743
|
+
"#pragma mark - Commands\n" + WDA_PHOTOS_COMMANDS),
|
|
744
|
+
# Transfers (phone_io/transfer.py): anchored on the Photos patch's own lines, applied after it.
|
|
745
|
+
("WebDriverAgentLib/Commands/FBCustomCommands.m", WDA_TRANSFER_IMPORT_ANCHOR, WDA_TRANSFER_IMPORT),
|
|
746
|
+
("WebDriverAgentLib/Commands/FBCustomCommands.m", WDA_TRANSFER_ROUTE_ANCHOR,
|
|
747
|
+
WDA_TRANSFER_ROUTES + WDA_TRANSFER_ROUTE_ANCHOR),
|
|
748
|
+
TRANSFER_COMMANDS_PATCH,
|
|
749
|
+
# the gate for screenshots and snapshots (the MJPEG/AX hang); private password entry
|
|
750
|
+
) + wda_gate.PATCHES + SECRET_PATCHES
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def active_patches(env=None):
|
|
754
|
+
"""WDA_PATCHES; with MOBSTER_QUEUE_PREFLIGHT on, the queue-check routes (PREFLIGHT_PATCHES) too, and the transfer
|
|
755
|
+
commands in their variant that names the upload's file with its extension."""
|
|
756
|
+
if not phone_ready.enabled(env):
|
|
757
|
+
return WDA_PATCHES
|
|
758
|
+
return tuple(TRANSFER_COMMANDS_NAMED_PATCH if patch == TRANSFER_COMMANDS_PATCH else patch
|
|
759
|
+
for patch in WDA_PATCHES) + PREFLIGHT_PATCHES
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
def patch_digest():
|
|
763
|
+
"""A fingerprint of every edit active_patches makes; a runner built with another may run unpatched, so isn't used.
|
|
764
|
+
Turning MOBSTER_QUEUE_PREFLIGHT on or off changes it, so each phone rebuilds its helper once (about 115 s)."""
|
|
765
|
+
return hashlib.sha256(json.dumps(active_patches(), separators=(",", ":")).encode()).hexdigest()[:16]
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def pristine(project, name, git=None):
|
|
769
|
+
"""``name`` as the checked-out release has it (``git show HEAD:name``), or None when git can't say."""
|
|
770
|
+
from .. import mac_tools
|
|
771
|
+
git = git or mac_tools.tool("git") or "git"
|
|
772
|
+
try:
|
|
773
|
+
result = subprocess.run([git, "-C", str(project), "show", f"HEAD:{name}"], capture_output=True, timeout=20,
|
|
774
|
+
stdin=subprocess.DEVNULL)
|
|
775
|
+
except (OSError, subprocess.SubprocessError):
|
|
776
|
+
return None
|
|
777
|
+
if result.returncode != 0:
|
|
778
|
+
return None
|
|
779
|
+
try:
|
|
780
|
+
return result.stdout.decode("utf-8")
|
|
781
|
+
except UnicodeDecodeError:
|
|
782
|
+
return None
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
def patch_wda(project, git=None):
|
|
786
|
+
"""Apply active_patches() to a clone: the patched files become exactly the pinned release's with these patches,
|
|
787
|
+
however
|
|
788
|
+
an earlier Mobster left them. A source they don't fit fails the build rather than run open.
|
|
789
|
+
|
|
790
|
+
Each file starts from the release's own text (``pristine``: git HEAD), never from what is on disk: the clone is
|
|
791
|
+
shared and kept between builds, and a patch whose text changed in an update would otherwise go in beside the old
|
|
792
|
+
one (two ``handleMobsterSecret:`` methods, a build that fails, 16 Oct). The edits are made in memory and written
|
|
793
|
+
only when every one fits. A tree that isn't a git checkout is patched from disk, and one an earlier Mobster patched
|
|
794
|
+
differently (it names Mobster, but this patch isn't there) is refused rather than patched twice."""
|
|
795
|
+
project = Path(project)
|
|
796
|
+
refused = RuntimeError(f"WebDriverAgent is not {wda_source.REF}, so it can't be limited to USB. "
|
|
797
|
+
f"Delete {project} and build again.")
|
|
798
|
+
patches = active_patches()
|
|
799
|
+
texts, from_disk = {}, set()
|
|
800
|
+
for name, _anchor, _patched in patches:
|
|
801
|
+
if name in texts:
|
|
802
|
+
continue
|
|
803
|
+
base = pristine(project, name, git)
|
|
804
|
+
if base is None:
|
|
805
|
+
base = (project / name).read_text()
|
|
806
|
+
from_disk.add(name)
|
|
807
|
+
texts[name] = base
|
|
808
|
+
# A file read from disk that already names Mobster was patched by a Mobster before this run.
|
|
809
|
+
patched_before = {name for name in from_disk if "mobster" in texts[name].lower()}
|
|
810
|
+
for name, anchor, patched in patches:
|
|
811
|
+
text = texts[name]
|
|
812
|
+
if patched in text:
|
|
813
|
+
continue
|
|
814
|
+
if name in patched_before:
|
|
815
|
+
raise refused # patched before with other edits: applying these too would duplicate them
|
|
816
|
+
if text.count(anchor) != 1:
|
|
817
|
+
raise refused
|
|
818
|
+
texts[name] = text.replace(anchor, patched)
|
|
819
|
+
for name, text in texts.items():
|
|
820
|
+
path = project / name
|
|
821
|
+
if path.read_text() != text:
|
|
822
|
+
path.write_text(text)
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
def xctestrun_targets(plan):
|
|
826
|
+
"""Test targets in either Xcode test-plan format; metadata is never a target."""
|
|
827
|
+
if not isinstance(plan, dict):
|
|
828
|
+
return []
|
|
829
|
+
configurations = plan.get("TestConfigurations")
|
|
830
|
+
if isinstance(configurations, list):
|
|
831
|
+
return [target for configuration in configurations if isinstance(configuration, dict)
|
|
832
|
+
for target in configuration.get("TestTargets") or () if isinstance(target, dict)]
|
|
833
|
+
return [value for key, value in plan.items() if not key.startswith("__") and isinstance(value, dict)]
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
def pin_loopback(products):
|
|
837
|
+
"""USE_IP=127.0.0.1 in built test plans: a build made before it was passed serves every interface."""
|
|
838
|
+
for path in Path(products).glob("*.xctestrun"):
|
|
839
|
+
try:
|
|
840
|
+
with open(path, "rb") as stream:
|
|
841
|
+
plan = plistlib.load(stream)
|
|
842
|
+
except (OSError, ValueError, plistlib.InvalidFileException):
|
|
843
|
+
continue
|
|
844
|
+
changed = False
|
|
845
|
+
for target in xctestrun_targets(plan):
|
|
846
|
+
env = target.get("EnvironmentVariables")
|
|
847
|
+
if not isinstance(env, dict):
|
|
848
|
+
env = target["EnvironmentVariables"] = {}
|
|
849
|
+
for section in (env, target.get("TestingEnvironmentVariables")):
|
|
850
|
+
if isinstance(section, dict) and (section is env or "USE_IP" in section) \
|
|
851
|
+
and section.get("USE_IP") != LOOPBACK:
|
|
852
|
+
section["USE_IP"] = LOOPBACK
|
|
853
|
+
changed = True
|
|
854
|
+
if changed:
|
|
855
|
+
with open(path, "wb") as stream:
|
|
856
|
+
plistlib.dump(plan, stream)
|