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.
Files changed (1009) hide show
  1. mobile_agent/__init__.py +4 -0
  2. mobile_agent/__main__.py +598 -0
  3. mobile_agent/accounts/__init__.py +1 -0
  4. mobile_agent/accounts/context.py +80 -0
  5. mobile_agent/accounts/mcp.py +55 -0
  6. mobile_agent/accounts/pacing.py +318 -0
  7. mobile_agent/accounts/register.py +78 -0
  8. mobile_agent/accounts/routes.py +212 -0
  9. mobile_agent/accounts/routing.py +69 -0
  10. mobile_agent/accounts/screen.py +273 -0
  11. mobile_agent/accounts/stats.py +54 -0
  12. mobile_agent/accounts/store.py +394 -0
  13. mobile_agent/agent_hooks.py +123 -0
  14. mobile_agent/ai_plans/__init__.py +7 -0
  15. mobile_agent/ai_plans/agents.py +96 -0
  16. mobile_agent/ai_plans/claude_code.py +208 -0
  17. mobile_agent/ai_plans/cli.py +400 -0
  18. mobile_agent/ai_plans/codex.py +258 -0
  19. mobile_agent/ai_plans/compat_chat.py +307 -0
  20. mobile_agent/ai_plans/device_login.py +164 -0
  21. mobile_agent/ai_plans/draft_client.py +68 -0
  22. mobile_agent/ai_plans/gemini.py +137 -0
  23. mobile_agent/ai_plans/instructions.py +65 -0
  24. mobile_agent/ai_plans/mcp_http.py +160 -0
  25. mobile_agent/ai_plans/phone_tools.py +892 -0
  26. mobile_agent/ai_plans/providers.py +235 -0
  27. mobile_agent/ai_plans/routes.py +160 -0
  28. mobile_agent/ai_plans/runner.py +400 -0
  29. mobile_agent/ai_plans/vault.py +114 -0
  30. mobile_agent/alerts.py +337 -0
  31. mobile_agent/android/__init__.py +18 -0
  32. mobile_agent/android/adb.py +268 -0
  33. mobile_agent/android/conditions.py +128 -0
  34. mobile_agent/android/doctor.py +54 -0
  35. mobile_agent/android/driver.py +137 -0
  36. mobile_agent/android/emulator.py +216 -0
  37. mobile_agent/android/logs.py +85 -0
  38. mobile_agent/android/manager.py +225 -0
  39. mobile_agent/android/records.py +31 -0
  40. mobile_agent/android/tree.py +96 -0
  41. mobile_agent/answer_shapes.py +230 -0
  42. mobile_agent/api_devices.py +333 -0
  43. mobile_agent/api_errors.py +17 -0
  44. mobile_agent/api_get.py +249 -0
  45. mobile_agent/api_http.py +467 -0
  46. mobile_agent/api_post.py +453 -0
  47. mobile_agent/api_routes.py +173 -0
  48. mobile_agent/app_api/__init__.py +4 -0
  49. mobile_agent/app_api/cli.py +151 -0
  50. mobile_agent/app_api/endpoints.py +193 -0
  51. mobile_agent/app_api/mcp.py +54 -0
  52. mobile_agent/app_api/openapi.py +146 -0
  53. mobile_agent/app_api/reading.py +172 -0
  54. mobile_agent/app_api/register.py +10 -0
  55. mobile_agent/app_api/screen.py +216 -0
  56. mobile_agent/app_api/service.py +382 -0
  57. mobile_agent/app_api/sessions.py +404 -0
  58. mobile_agent/app_api/tokens.py +167 -0
  59. mobile_agent/app_choice.py +131 -0
  60. mobile_agent/app_icons.py +183 -0
  61. mobile_agent/app_killer.py +69 -0
  62. mobile_agent/artifacts.py +606 -0
  63. mobile_agent/attach/__init__.py +13 -0
  64. mobile_agent/attach/cli.py +46 -0
  65. mobile_agent/attach/flutter.py +28 -0
  66. mobile_agent/attach/score.py +119 -0
  67. mobile_agent/attach/web.py +13 -0
  68. mobile_agent/attachments/__init__.py +1 -0
  69. mobile_agent/attachments/agent.py +418 -0
  70. mobile_agent/attachments/extract.py +217 -0
  71. mobile_agent/attachments/mcp.py +104 -0
  72. mobile_agent/attachments/register.py +73 -0
  73. mobile_agent/attachments/routes.py +326 -0
  74. mobile_agent/attachments/service.py +170 -0
  75. mobile_agent/attachments/sniff.py +152 -0
  76. mobile_agent/attachments/store.py +441 -0
  77. mobile_agent/audit_verify.py +436 -0
  78. mobile_agent/bench/README.md +410 -0
  79. mobile_agent/bench/__init__.py +0 -0
  80. mobile_agent/bench/__main__.py +394 -0
  81. mobile_agent/bench/ab.py +168 -0
  82. mobile_agent/bench/actuation.py +368 -0
  83. mobile_agent/bench/agents/__init__.py +9 -0
  84. mobile_agent/bench/agents/base.py +105 -0
  85. mobile_agent/bench/agents/plan_clients.py +245 -0
  86. mobile_agent/bench/agents/smart.py +533 -0
  87. mobile_agent/bench/checks.py +358 -0
  88. mobile_agent/bench/decide_gate.py +214 -0
  89. mobile_agent/bench/diagnose.py +181 -0
  90. mobile_agent/bench/evals/__init__.py +14 -0
  91. mobile_agent/bench/evals/build.py +565 -0
  92. mobile_agent/bench/evals/checkpoint.py +214 -0
  93. mobile_agent/bench/evals/irt.py +219 -0
  94. mobile_agent/bench/evals/l0.py +647 -0
  95. mobile_agent/bench/evals/l1.py +866 -0
  96. mobile_agent/bench/evals/models.py +124 -0
  97. mobile_agent/bench/evals/pairwise.py +189 -0
  98. mobile_agent/bench/evals/proxy.py +90 -0
  99. mobile_agent/bench/evals/sets.py +149 -0
  100. mobile_agent/bench/evals/traces.py +446 -0
  101. mobile_agent/bench/fixtures.py +176 -0
  102. mobile_agent/bench/guard_replay.py +129 -0
  103. mobile_agent/bench/h2-manifest.json +20 -0
  104. mobile_agent/bench/h3-manifest.json +20 -0
  105. mobile_agent/bench/heldout-manifest.json +32 -0
  106. mobile_agent/bench/heldout.py +375 -0
  107. mobile_agent/bench/iosworld.py +901 -0
  108. mobile_agent/bench/iosworld_stats.py +510 -0
  109. mobile_agent/bench/llm_units.py +116 -0
  110. mobile_agent/bench/manifest.json +93 -0
  111. mobile_agent/bench/metrics.py +66 -0
  112. mobile_agent/bench/phone.py +44 -0
  113. mobile_agent/bench/replay.py +172 -0
  114. mobile_agent/bench/report.py +207 -0
  115. mobile_agent/bench/runner.py +456 -0
  116. mobile_agent/bench/safety.py +310 -0
  117. mobile_agent/bench/stream_timing.py +213 -0
  118. mobile_agent/bench/suite.py +444 -0
  119. mobile_agent/bench/sweep.py +176 -0
  120. mobile_agent/bench/tape.py +798 -0
  121. mobile_agent/bench/tape_replay.py +88 -0
  122. mobile_agent/bench/trace_evals.py +128 -0
  123. mobile_agent/bench/trace_view.py +81 -0
  124. mobile_agent/bench/truth/iphone15pro.json +138 -0
  125. mobile_agent/bench/truth.py +340 -0
  126. mobile_agent/bench/tuning.py +543 -0
  127. mobile_agent/bench/tuning_analysis.py +656 -0
  128. mobile_agent/bench/vague-heldout-manifest.json +12 -0
  129. mobile_agent/bench/vague_rubric.py +135 -0
  130. mobile_agent/bench/vague_run.py +243 -0
  131. mobile_agent/brand.py +454 -0
  132. mobile_agent/brand_fonts.py +756 -0
  133. mobile_agent/build_identity.py +21 -0
  134. mobile_agent/cable_bt.py +623 -0
  135. mobile_agent/cable_bt_runtime.py +128 -0
  136. mobile_agent/catalog.py +69 -0
  137. mobile_agent/chart_svg.py +415 -0
  138. mobile_agent/chat_here.py +189 -0
  139. mobile_agent/cli_constants.py +5 -0
  140. mobile_agent/cli_parser.py +611 -0
  141. mobile_agent/cli_run.py +115 -0
  142. mobile_agent/compiled_read/__init__.py +21 -0
  143. mobile_agent/compiled_read/delta.py +89 -0
  144. mobile_agent/compiled_read/extractors.py +202 -0
  145. mobile_agent/compiled_read/routes.py +56 -0
  146. mobile_agent/completion.py +267 -0
  147. mobile_agent/compose.py +48 -0
  148. mobile_agent/config.py +114 -0
  149. mobile_agent/connectors/__init__.py +1 -0
  150. mobile_agent/connectors/api_session.py +76 -0
  151. mobile_agent/connectors/callback_page.py +146 -0
  152. mobile_agent/connectors/client.py +582 -0
  153. mobile_agent/connectors/github_app.py +55 -0
  154. mobile_agent/connectors/graph_api.py +396 -0
  155. mobile_agent/connectors/imports.py +170 -0
  156. mobile_agent/connectors/issue.py +66 -0
  157. mobile_agent/connectors/keystore.py +85 -0
  158. mobile_agent/connectors/marks.py +222 -0
  159. mobile_agent/connectors/oauth.py +476 -0
  160. mobile_agent/connectors/picker.py +228 -0
  161. mobile_agent/connectors/presets.py +591 -0
  162. mobile_agent/connectors/register.py +48 -0
  163. mobile_agent/connectors/routes.py +153 -0
  164. mobile_agent/connectors/service.py +966 -0
  165. mobile_agent/connectors/shared.py +136 -0
  166. mobile_agent/connectors/skill.py +446 -0
  167. mobile_agent/connectors/slack_api.py +269 -0
  168. mobile_agent/connectors/steps.py +92 -0
  169. mobile_agent/connectors/store.py +144 -0
  170. mobile_agent/connectors/vault.py +72 -0
  171. mobile_agent/console.py +178 -0
  172. mobile_agent/content_queue/__init__.py +1 -0
  173. mobile_agent/content_queue/client.py +263 -0
  174. mobile_agent/content_queue/importing.py +153 -0
  175. mobile_agent/content_queue/media_check.py +224 -0
  176. mobile_agent/content_queue/prompt.py +64 -0
  177. mobile_agent/content_queue/register.py +91 -0
  178. mobile_agent/content_queue/routes.py +370 -0
  179. mobile_agent/content_queue/runner.py +475 -0
  180. mobile_agent/content_queue/store.py +521 -0
  181. mobile_agent/contract.py +996 -0
  182. mobile_agent/contract_asks.py +248 -0
  183. mobile_agent/contract_prompt.py +45 -0
  184. mobile_agent/contract_screen.py +981 -0
  185. mobile_agent/contract_view.py +84 -0
  186. mobile_agent/corpus/__init__.py +26 -0
  187. mobile_agent/corpus/cli.py +63 -0
  188. mobile_agent/corpus/consent.py +55 -0
  189. mobile_agent/corpus/listener.py +76 -0
  190. mobile_agent/corpus/register.py +9 -0
  191. mobile_agent/corpus/skeleton.py +72 -0
  192. mobile_agent/corpus/store.py +143 -0
  193. mobile_agent/costs.py +161 -0
  194. mobile_agent/decider_scope.py +40 -0
  195. mobile_agent/decision_provider.py +189 -0
  196. mobile_agent/deeplinks.py +352 -0
  197. mobile_agent/device_info.py +116 -0
  198. mobile_agent/device_manager.py +978 -0
  199. mobile_agent/device_status.py +195 -0
  200. mobile_agent/device_targets.py +217 -0
  201. mobile_agent/devices.py +676 -0
  202. mobile_agent/devkit/__init__.py +64 -0
  203. mobile_agent/devkit/account_cli.py +90 -0
  204. mobile_agent/devkit/asc.py +562 -0
  205. mobile_agent/devkit/asc_cli.py +214 -0
  206. mobile_agent/devkit/ci.py +737 -0
  207. mobile_agent/devkit/ci_cli.py +196 -0
  208. mobile_agent/devkit/ci_hosts.py +194 -0
  209. mobile_agent/devkit/es256.py +197 -0
  210. mobile_agent/devkit/expo.py +440 -0
  211. mobile_agent/devkit/expo_cli.py +405 -0
  212. mobile_agent/devkit/expo_dev.py +203 -0
  213. mobile_agent/devkit/expo_routes.py +90 -0
  214. mobile_agent/devkit/fixtures_cli.py +133 -0
  215. mobile_agent/devkit/fixtures_routes.py +82 -0
  216. mobile_agent/devkit/links.py +375 -0
  217. mobile_agent/devkit/links_cli.py +123 -0
  218. mobile_agent/devkit/links_routes.py +83 -0
  219. mobile_agent/devkit/nightly.py +152 -0
  220. mobile_agent/devkit/shots.py +203 -0
  221. mobile_agent/devkit/testflight.py +155 -0
  222. mobile_agent/devkit/testflight_cli.py +127 -0
  223. mobile_agent/devkit/testflight_routes.py +111 -0
  224. mobile_agent/devkit/xcode.py +236 -0
  225. mobile_agent/devkit/xcode_cli.py +101 -0
  226. mobile_agent/devtools.py +419 -0
  227. mobile_agent/doctor.py +509 -0
  228. mobile_agent/doctor_fix.py +467 -0
  229. mobile_agent/downloads.py +55 -0
  230. mobile_agent/drivers.py +793 -0
  231. mobile_agent/dry_run.py +102 -0
  232. mobile_agent/effect_ledger.py +83 -0
  233. mobile_agent/engine_health.py +378 -0
  234. mobile_agent/engine_hooks.py +96 -0
  235. mobile_agent/engines.py +408 -0
  236. mobile_agent/env_file.py +63 -0
  237. mobile_agent/env_secrets.py +149 -0
  238. mobile_agent/errors.py +15 -0
  239. mobile_agent/evals/__init__.py +3 -0
  240. mobile_agent/evals/app_survey.py +149 -0
  241. mobile_agent/evals/development/__init__.py +1 -0
  242. mobile_agent/evals/development/score.py +276 -0
  243. mobile_agent/evals/latency_report.py +312 -0
  244. mobile_agent/evals/offline_report.py +374 -0
  245. mobile_agent/evals/oracles.py +298 -0
  246. mobile_agent/evals/redteam/__init__.py +15 -0
  247. mobile_agent/evals/redteam/__main__.py +72 -0
  248. mobile_agent/evals/redteam/cases.py +730 -0
  249. mobile_agent/evals/redteam/harness.py +604 -0
  250. mobile_agent/evals/redteam/live.py +139 -0
  251. mobile_agent/evals/redteam/plain.py +85 -0
  252. mobile_agent/evals/vision/__init__.py +0 -0
  253. mobile_agent/evals/vision/images/dog_001.jpg +0 -0
  254. mobile_agent/evals/vision/images/dog_002.jpg +0 -0
  255. mobile_agent/evals/vision/images/dog_003.jpg +0 -0
  256. mobile_agent/evals/vision/images/dog_004.jpg +0 -0
  257. mobile_agent/evals/vision/images/dog_005.jpg +0 -0
  258. mobile_agent/evals/vision/images/dog_006.jpg +0 -0
  259. mobile_agent/evals/vision/images/dog_007.jpg +0 -0
  260. mobile_agent/evals/vision/images/dog_010.jpg +0 -0
  261. mobile_agent/evals/vision/images/dog_011.jpg +0 -0
  262. mobile_agent/evals/vision/images/dog_013.jpg +0 -0
  263. mobile_agent/evals/vision/images/dog_014.jpg +0 -0
  264. mobile_agent/evals/vision/images/dog_015.jpg +0 -0
  265. mobile_agent/evals/vision/images/dog_016.jpg +0 -0
  266. mobile_agent/evals/vision/images/dog_017.jpg +0 -0
  267. mobile_agent/evals/vision/images/dog_019.jpg +0 -0
  268. mobile_agent/evals/vision/images/dog_020.jpg +0 -0
  269. mobile_agent/evals/vision/images/dog_021.jpg +0 -0
  270. mobile_agent/evals/vision/images/dog_022.jpg +0 -0
  271. mobile_agent/evals/vision/images/dog_023.jpg +0 -0
  272. mobile_agent/evals/vision/images/dog_024.jpg +0 -0
  273. mobile_agent/evals/vision/images/dog_026.jpg +0 -0
  274. mobile_agent/evals/vision/images/dog_027.jpg +0 -0
  275. mobile_agent/evals/vision/images/dog_034.jpg +0 -0
  276. mobile_agent/evals/vision/images/dog_035.jpg +0 -0
  277. mobile_agent/evals/vision/images/dog_038.jpg +0 -0
  278. mobile_agent/evals/vision/images/dog_042.jpg +0 -0
  279. mobile_agent/evals/vision/images/dog_047.jpg +0 -0
  280. mobile_agent/evals/vision/images/dog_052.jpg +0 -0
  281. mobile_agent/evals/vision/images/dog_058.jpg +0 -0
  282. mobile_agent/evals/vision/images/dog_062.jpg +0 -0
  283. mobile_agent/evals/vision/images/eyes_000.jpg +0 -0
  284. mobile_agent/evals/vision/images/eyes_010.jpg +0 -0
  285. mobile_agent/evals/vision/images/eyes_011.jpg +0 -0
  286. mobile_agent/evals/vision/images/eyes_015.jpg +0 -0
  287. mobile_agent/evals/vision/images/eyes_016.jpg +0 -0
  288. mobile_agent/evals/vision/images/eyes_017.jpg +0 -0
  289. mobile_agent/evals/vision/images/eyes_019.jpg +0 -0
  290. mobile_agent/evals/vision/images/eyes_021.jpg +0 -0
  291. mobile_agent/evals/vision/images/eyes_025.jpg +0 -0
  292. mobile_agent/evals/vision/images/eyes_026.jpg +0 -0
  293. mobile_agent/evals/vision/images/eyes_027.jpg +0 -0
  294. mobile_agent/evals/vision/images/eyes_030.jpg +0 -0
  295. mobile_agent/evals/vision/images/eyes_032.jpg +0 -0
  296. mobile_agent/evals/vision/images/eyes_033.jpg +0 -0
  297. mobile_agent/evals/vision/images/eyes_034.jpg +0 -0
  298. mobile_agent/evals/vision/images/eyes_035.jpg +0 -0
  299. mobile_agent/evals/vision/images/eyes_036.jpg +0 -0
  300. mobile_agent/evals/vision/images/eyes_037.jpg +0 -0
  301. mobile_agent/evals/vision/images/eyes_039.jpg +0 -0
  302. mobile_agent/evals/vision/images/eyes_040.jpg +0 -0
  303. mobile_agent/evals/vision/images/eyes_041.jpg +0 -0
  304. mobile_agent/evals/vision/images/eyes_042.jpg +0 -0
  305. mobile_agent/evals/vision/images/eyes_043.jpg +0 -0
  306. mobile_agent/evals/vision/images/eyes_044.jpg +0 -0
  307. mobile_agent/evals/vision/images/eyes_046.jpg +0 -0
  308. mobile_agent/evals/vision/images/eyes_047.jpg +0 -0
  309. mobile_agent/evals/vision/images/eyes_048.jpg +0 -0
  310. mobile_agent/evals/vision/images/eyes_049.jpg +0 -0
  311. mobile_agent/evals/vision/images/eyes_051.jpg +0 -0
  312. mobile_agent/evals/vision/images/eyes_053.jpg +0 -0
  313. mobile_agent/evals/vision/images/eyes_054.jpg +0 -0
  314. mobile_agent/evals/vision/images/eyes_055.jpg +0 -0
  315. mobile_agent/evals/vision/images/eyes_058.jpg +0 -0
  316. mobile_agent/evals/vision/images/eyes_059.jpg +0 -0
  317. mobile_agent/evals/vision/images/receipt_002.jpg +0 -0
  318. mobile_agent/evals/vision/images/receipt_003.jpg +0 -0
  319. mobile_agent/evals/vision/images/receipt_004.jpg +0 -0
  320. mobile_agent/evals/vision/images/receipt_007.jpg +0 -0
  321. mobile_agent/evals/vision/images/receipt_009.jpg +0 -0
  322. mobile_agent/evals/vision/images/receipt_010.jpg +0 -0
  323. mobile_agent/evals/vision/images/receipt_011.jpg +0 -0
  324. mobile_agent/evals/vision/images/receipt_012.jpg +0 -0
  325. mobile_agent/evals/vision/images/receipt_013.jpg +0 -0
  326. mobile_agent/evals/vision/images/receipt_014.jpg +0 -0
  327. mobile_agent/evals/vision/images/receipt_016.jpg +0 -0
  328. mobile_agent/evals/vision/images/receipt_017.jpg +0 -0
  329. mobile_agent/evals/vision/images/receipt_023.jpg +0 -0
  330. mobile_agent/evals/vision/images/receipt_027.jpg +0 -0
  331. mobile_agent/evals/vision/images/receipt_029.jpg +0 -0
  332. mobile_agent/evals/vision/images/receipt_032.jpg +0 -0
  333. mobile_agent/evals/vision/images/receipt_033.jpg +0 -0
  334. mobile_agent/evals/vision/images/receipt_036.jpg +0 -0
  335. mobile_agent/evals/vision/images/receipt_041.jpg +0 -0
  336. mobile_agent/evals/vision/images/receipt_043.jpg +0 -0
  337. mobile_agent/evals/vision/images/receipt_047.jpg +0 -0
  338. mobile_agent/evals/vision/images/receipt_049.jpg +0 -0
  339. mobile_agent/evals/vision/images/receipt_051.jpg +0 -0
  340. mobile_agent/evals/vision/images/receipt_053.jpg +0 -0
  341. mobile_agent/evals/vision/images/receipt_056.jpg +0 -0
  342. mobile_agent/evals/vision/images/receipt_057.jpg +0 -0
  343. mobile_agent/evals/vision/images/receipt_058.jpg +0 -0
  344. mobile_agent/evals/vision/images/receipt_059.jpg +0 -0
  345. mobile_agent/evals/vision/images/receipt_060.jpg +0 -0
  346. mobile_agent/evals/vision/images/receipt_061.jpg +0 -0
  347. mobile_agent/evals/vision/images/receipt_062.jpg +0 -0
  348. mobile_agent/evals/vision/images/receipt_063.jpg +0 -0
  349. mobile_agent/evals/vision/images/receipt_064.jpg +0 -0
  350. mobile_agent/evals/vision/images/receipt_065.jpg +0 -0
  351. mobile_agent/evals/vision/images/receipt_066.jpg +0 -0
  352. mobile_agent/evals/vision/images/receipt_067.jpg +0 -0
  353. mobile_agent/evals/vision/images/receipt_068.jpg +0 -0
  354. mobile_agent/evals/vision/images/receipt_069.jpg +0 -0
  355. mobile_agent/evals/vision/images/receipt_070.jpg +0 -0
  356. mobile_agent/evals/vision/images/receipt_071.jpg +0 -0
  357. mobile_agent/evals/vision/images/sunglasses_000.jpg +0 -0
  358. mobile_agent/evals/vision/images/sunglasses_001.jpg +0 -0
  359. mobile_agent/evals/vision/images/sunglasses_002.jpg +0 -0
  360. mobile_agent/evals/vision/images/sunglasses_004.jpg +0 -0
  361. mobile_agent/evals/vision/images/sunglasses_005.jpg +0 -0
  362. mobile_agent/evals/vision/images/sunglasses_008.jpg +0 -0
  363. mobile_agent/evals/vision/images/sunglasses_009.jpg +0 -0
  364. mobile_agent/evals/vision/images/sunglasses_010.jpg +0 -0
  365. mobile_agent/evals/vision/images/sunglasses_012.jpg +0 -0
  366. mobile_agent/evals/vision/images/sunglasses_013.jpg +0 -0
  367. mobile_agent/evals/vision/images/sunglasses_015.jpg +0 -0
  368. mobile_agent/evals/vision/images/sunglasses_016.jpg +0 -0
  369. mobile_agent/evals/vision/images/sunglasses_017.jpg +0 -0
  370. mobile_agent/evals/vision/images/sunglasses_018.jpg +0 -0
  371. mobile_agent/evals/vision/images/sunglasses_021.jpg +0 -0
  372. mobile_agent/evals/vision/images/sunglasses_022.jpg +0 -0
  373. mobile_agent/evals/vision/images/sunglasses_023.jpg +0 -0
  374. mobile_agent/evals/vision/images/sunglasses_024.jpg +0 -0
  375. mobile_agent/evals/vision/images/sunglasses_030.jpg +0 -0
  376. mobile_agent/evals/vision/images/sunglasses_031.jpg +0 -0
  377. mobile_agent/evals/vision/images/sunglasses_032.jpg +0 -0
  378. mobile_agent/evals/vision/images/sunglasses_033.jpg +0 -0
  379. mobile_agent/evals/vision/images/sunglasses_034.jpg +0 -0
  380. mobile_agent/evals/vision/images/sunglasses_036.jpg +0 -0
  381. mobile_agent/evals/vision/images/sunglasses_038.jpg +0 -0
  382. mobile_agent/evals/vision/images/sunglasses_039.jpg +0 -0
  383. mobile_agent/evals/vision/images/sunglasses_040.jpg +0 -0
  384. mobile_agent/evals/vision/images/sunglasses_041.jpg +0 -0
  385. mobile_agent/evals/vision/manifest.json +2355 -0
  386. mobile_agent/eventbus.py +110 -0
  387. mobile_agent/explain.py +34 -0
  388. mobile_agent/failure_line.py +197 -0
  389. mobile_agent/fanout.py +663 -0
  390. mobile_agent/feedback/__init__.py +2 -0
  391. mobile_agent/feedback/bundle.py +204 -0
  392. mobile_agent/feedback/outbox.py +196 -0
  393. mobile_agent/feedback/register.py +9 -0
  394. mobile_agent/feedback/routes.py +177 -0
  395. mobile_agent/fleet.py +841 -0
  396. mobile_agent/fleet_health.py +729 -0
  397. mobile_agent/fleet_phone.py +186 -0
  398. mobile_agent/frontier/__init__.py +79 -0
  399. mobile_agent/frontier/act.py +380 -0
  400. mobile_agent/frontier/actions.py +70 -0
  401. mobile_agent/frontier/agent.py +256 -0
  402. mobile_agent/frontier/approvals.py +366 -0
  403. mobile_agent/frontier/hooks.py +199 -0
  404. mobile_agent/frontier/interrupts.py +125 -0
  405. mobile_agent/frontier/observe.py +331 -0
  406. mobile_agent/frontier/phases.py +381 -0
  407. mobile_agent/frontier/pipeline.py +61 -0
  408. mobile_agent/frontier/prompt.py +384 -0
  409. mobile_agent/frontier/protocol.py +91 -0
  410. mobile_agent/frontier/safety.py +467 -0
  411. mobile_agent/frontier/saved.py +375 -0
  412. mobile_agent/frontier/screen.py +419 -0
  413. mobile_agent/frontier/skills.py +147 -0
  414. mobile_agent/frontier/stages/__init__.py +1 -0
  415. mobile_agent/frontier/stages/after.py +192 -0
  416. mobile_agent/frontier/stages/approve.py +138 -0
  417. mobile_agent/frontier/stages/controls.py +115 -0
  418. mobile_agent/frontier/stages/ends.py +102 -0
  419. mobile_agent/frontier/stages/gate.py +92 -0
  420. mobile_agent/frontier/stages/guards.py +132 -0
  421. mobile_agent/frontier/stages/points.py +155 -0
  422. mobile_agent/frontier/stages/send.py +177 -0
  423. mobile_agent/frontier/turn.py +151 -0
  424. mobile_agent/full_disk_access.py +83 -0
  425. mobile_agent/gemini.py +69 -0
  426. mobile_agent/genuine.py +120 -0
  427. mobile_agent/graph/__init__.py +4 -0
  428. mobile_agent/graph/catalog.py +789 -0
  429. mobile_agent/graph/chain.py +306 -0
  430. mobile_agent/graph/dispatch.py +98 -0
  431. mobile_agent/graph/draft.py +373 -0
  432. mobile_agent/graph/edit.py +76 -0
  433. mobile_agent/graph/estimate.py +189 -0
  434. mobile_agent/graph/expr.py +562 -0
  435. mobile_agent/graph/functions.py +271 -0
  436. mobile_agent/graph/infer.py +144 -0
  437. mobile_agent/graph/nodes.py +936 -0
  438. mobile_agent/graph/phone_steps.py +178 -0
  439. mobile_agent/graph/rerun.py +327 -0
  440. mobile_agent/graph/run.py +899 -0
  441. mobile_agent/graph/schema.py +842 -0
  442. mobile_agent/graph/state.py +161 -0
  443. mobile_agent/graph/values.py +198 -0
  444. mobile_agent/harness/__init__.py +2 -0
  445. mobile_agent/harness/checkpoints.py +468 -0
  446. mobile_agent/harness/control.py +101 -0
  447. mobile_agent/harness/prewarm.py +213 -0
  448. mobile_agent/harness/register.py +43 -0
  449. mobile_agent/harness/routes.py +114 -0
  450. mobile_agent/harness/settings.py +66 -0
  451. mobile_agent/harness/tools/__init__.py +67 -0
  452. mobile_agent/harness/tools/ask_user.py +102 -0
  453. mobile_agent/harness/tools/watch.py +120 -0
  454. mobile_agent/harness_api.py +815 -0
  455. mobile_agent/hedge.py +203 -0
  456. mobile_agent/help_topics.py +305 -0
  457. mobile_agent/hints.py +248 -0
  458. mobile_agent/history.py +69 -0
  459. mobile_agent/home.py +214 -0
  460. mobile_agent/http_pool.py +175 -0
  461. mobile_agent/image_policy.py +139 -0
  462. mobile_agent/inbox.py +148 -0
  463. mobile_agent/installed_apps.py +344 -0
  464. mobile_agent/integrations/__init__.py +1 -0
  465. mobile_agent/integrations/cli.py +472 -0
  466. mobile_agent/integrations/client_files.py +65 -0
  467. mobile_agent/integrations/clients.py +998 -0
  468. mobile_agent/integrations/doctor.py +185 -0
  469. mobile_agent/integrations/jsonc.py +383 -0
  470. mobile_agent/integrations/routes.py +135 -0
  471. mobile_agent/integrations/rules.py +112 -0
  472. mobile_agent/integrations/server_options.py +46 -0
  473. mobile_agent/integrations/skill.py +152 -0
  474. mobile_agent/integrations/skill_files.py +10 -0
  475. mobile_agent/integrations/textedit.py +309 -0
  476. mobile_agent/intents/__init__.py +17 -0
  477. mobile_agent/intents/atlas.py +216 -0
  478. mobile_agent/intents/callback.py +44 -0
  479. mobile_agent/intents/cli.py +55 -0
  480. mobile_agent/intents/harvest.py +91 -0
  481. mobile_agent/intents/plan.py +93 -0
  482. mobile_agent/ios/__init__.py +1 -0
  483. mobile_agent/ios/guide.py +16 -0
  484. mobile_agent/ios/kb.py +212 -0
  485. mobile_agent/ios/kb_data.py +1588 -0
  486. mobile_agent/ios/mcp.py +44 -0
  487. mobile_agent/ios/register.py +8 -0
  488. mobile_agent/ios/system.py +369 -0
  489. mobile_agent/journal.py +531 -0
  490. mobile_agent/keychain_acl.py +374 -0
  491. mobile_agent/keys.py +395 -0
  492. mobile_agent/latency_overlap.py +36 -0
  493. mobile_agent/latency_trace.py +170 -0
  494. mobile_agent/learned/__init__.py +15 -0
  495. mobile_agent/learned/app_path.py +160 -0
  496. mobile_agent/learned/code_read.py +38 -0
  497. mobile_agent/learned/parts.py +546 -0
  498. mobile_agent/learned/programs.py +691 -0
  499. mobile_agent/learned/register.py +171 -0
  500. mobile_agent/learned/routes.py +61 -0
  501. mobile_agent/learned/taught.py +329 -0
  502. mobile_agent/learned/teach.py +538 -0
  503. mobile_agent/learned/workflows.py +647 -0
  504. mobile_agent/learned/zero.py +41 -0
  505. mobile_agent/links.py +37 -0
  506. mobile_agent/listen.py +607 -0
  507. mobile_agent/lock_spike.py +278 -0
  508. mobile_agent/lockscreen.py +526 -0
  509. mobile_agent/login.py +354 -0
  510. mobile_agent/longform/__init__.py +15 -0
  511. mobile_agent/longform/ads.py +191 -0
  512. mobile_agent/longform/blocks.py +117 -0
  513. mobile_agent/longform/budget.py +265 -0
  514. mobile_agent/longform/context.py +285 -0
  515. mobile_agent/longform/costs.py +93 -0
  516. mobile_agent/longform/digest.py +145 -0
  517. mobile_agent/longform/extract.py +508 -0
  518. mobile_agent/longform/feed.py +505 -0
  519. mobile_agent/longform/followup.py +168 -0
  520. mobile_agent/longform/frames.py +82 -0
  521. mobile_agent/longform/identity.py +87 -0
  522. mobile_agent/longform/inbox.py +221 -0
  523. mobile_agent/longform/items.py +425 -0
  524. mobile_agent/longform/models.py +115 -0
  525. mobile_agent/longform/notebook.py +329 -0
  526. mobile_agent/longform/plan.py +614 -0
  527. mobile_agent/longform/recall.py +195 -0
  528. mobile_agent/longform/receipts.py +81 -0
  529. mobile_agent/longform/register.py +124 -0
  530. mobile_agent/longform/report.py +343 -0
  531. mobile_agent/longform/routes.py +102 -0
  532. mobile_agent/longform/session.py +644 -0
  533. mobile_agent/longform/store.py +146 -0
  534. mobile_agent/longform/synthesis.py +347 -0
  535. mobile_agent/mac_tools.py +223 -0
  536. mobile_agent/manual_control.py +143 -0
  537. mobile_agent/maps.py +149 -0
  538. mobile_agent/marks.py +92 -0
  539. mobile_agent/mcp_server/__init__.py +3 -0
  540. mobile_agent/mcp_server/__main__.py +29 -0
  541. mobile_agent/mcp_server/action_journal.py +24 -0
  542. mobile_agent/mcp_server/action_tools.py +638 -0
  543. mobile_agent/mcp_server/approvals.py +219 -0
  544. mobile_agent/mcp_server/catalog.py +92 -0
  545. mobile_agent/mcp_server/cli.py +194 -0
  546. mobile_agent/mcp_server/codes.py +147 -0
  547. mobile_agent/mcp_server/device_tools.py +306 -0
  548. mobile_agent/mcp_server/direct.py +272 -0
  549. mobile_agent/mcp_server/expo_tools.py +105 -0
  550. mobile_agent/mcp_server/phone_tools.py +510 -0
  551. mobile_agent/mcp_server/protocol.py +364 -0
  552. mobile_agent/mcp_server/registry.py +55 -0
  553. mobile_agent/mcp_server/run_tools.py +531 -0
  554. mobile_agent/mcp_server/schemas.py +425 -0
  555. mobile_agent/mcp_server/screen_tools.py +168 -0
  556. mobile_agent/mcp_server/session.py +469 -0
  557. mobile_agent/mcp_server/support.py +145 -0
  558. mobile_agent/mcp_server/tools.py +493 -0
  559. mobile_agent/memory/__init__.py +9 -0
  560. mobile_agent/memory/cli.py +207 -0
  561. mobile_agent/memory/extract.py +191 -0
  562. mobile_agent/memory/listener.py +100 -0
  563. mobile_agent/memory/model.py +144 -0
  564. mobile_agent/memory/project.py +96 -0
  565. mobile_agent/memory/provider.py +127 -0
  566. mobile_agent/memory/register.py +21 -0
  567. mobile_agent/memory/retrieval.py +172 -0
  568. mobile_agent/memory/routes.py +216 -0
  569. mobile_agent/memory/runs.py +124 -0
  570. mobile_agent/memory/store.py +806 -0
  571. mobile_agent/memory/writer.py +278 -0
  572. mobile_agent/mirroring.py +585 -0
  573. mobile_agent/mirroring_runtime.py +221 -0
  574. mobile_agent/model_clients.py +604 -0
  575. mobile_agent/narrate.py +813 -0
  576. mobile_agent/native_helpers.py +84 -0
  577. mobile_agent/netxray/__init__.py +1 -0
  578. mobile_agent/netxray/analyze.py +322 -0
  579. mobile_agent/netxray/attribution.py +62 -0
  580. mobile_agent/netxray/capture.py +88 -0
  581. mobile_agent/netxray/cli.py +142 -0
  582. mobile_agent/netxray/job.py +219 -0
  583. mobile_agent/netxray/mobster-pcap.c +560 -0
  584. mobile_agent/netxray/packets.py +410 -0
  585. mobile_agent/netxray/pcapng.py +223 -0
  586. mobile_agent/netxray/register.py +93 -0
  587. mobile_agent/netxray/report.py +214 -0
  588. mobile_agent/netxray/store.py +48 -0
  589. mobile_agent/netxray/summary.py +157 -0
  590. mobile_agent/netxray/target.py +67 -0
  591. mobile_agent/netxray/tool.py +66 -0
  592. mobile_agent/netxray/trackers.py +194 -0
  593. mobile_agent/notify_triggers/__init__.py +1 -0
  594. mobile_agent/notify_triggers/rules.py +39 -0
  595. mobile_agent/notify_triggers/sources.py +315 -0
  596. mobile_agent/notify_triggers/watch.py +135 -0
  597. mobile_agent/offers.py +86 -0
  598. mobile_agent/orchestrate.py +215 -0
  599. mobile_agent/outlets/__init__.py +1 -0
  600. mobile_agent/outlets/connect.py +249 -0
  601. mobile_agent/outlets/folder.py +180 -0
  602. mobile_agent/outlets/hooks.py +453 -0
  603. mobile_agent/outlets/inbound.py +472 -0
  604. mobile_agent/outlets/inbound_reply.py +57 -0
  605. mobile_agent/outlets/inbound_routes.py +81 -0
  606. mobile_agent/outlets/kinds.py +308 -0
  607. mobile_agent/outlets/mail.py +110 -0
  608. mobile_agent/outlets/net.py +86 -0
  609. mobile_agent/outlets/notion.py +244 -0
  610. mobile_agent/outlets/register.py +56 -0
  611. mobile_agent/outlets/routes.py +135 -0
  612. mobile_agent/outlets/service.py +580 -0
  613. mobile_agent/outlets/sheets.py +169 -0
  614. mobile_agent/outlets/slack.py +177 -0
  615. mobile_agent/outlets/store.py +194 -0
  616. mobile_agent/outlets/vault.py +80 -0
  617. mobile_agent/outlets/webhook.py +316 -0
  618. mobile_agent/output_contract.py +141 -0
  619. mobile_agent/owner_presence.py +211 -0
  620. mobile_agent/ownership.py +219 -0
  621. mobile_agent/packs/__init__.py +1 -0
  622. mobile_agent/packs/calendar.py +243 -0
  623. mobile_agent/packs/context.py +68 -0
  624. mobile_agent/packs/core.py +757 -0
  625. mobile_agent/packs/front_door.py +316 -0
  626. mobile_agent/packs/heal.py +214 -0
  627. mobile_agent/packs/health.py +122 -0
  628. mobile_agent/packs/insights.py +76 -0
  629. mobile_agent/packs/instagram.py +640 -0
  630. mobile_agent/packs/linkedin.py +106 -0
  631. mobile_agent/packs/maps.py +196 -0
  632. mobile_agent/packs/mcp.py +149 -0
  633. mobile_agent/packs/messages.py +270 -0
  634. mobile_agent/packs/notes.py +226 -0
  635. mobile_agent/packs/photos.py +166 -0
  636. mobile_agent/packs/pins.py +47 -0
  637. mobile_agent/packs/reddit.py +110 -0
  638. mobile_agent/packs/register.py +276 -0
  639. mobile_agent/packs/rows.py +108 -0
  640. mobile_agent/packs/safari.py +143 -0
  641. mobile_agent/packs/settings.py +253 -0
  642. mobile_agent/packs/social.py +300 -0
  643. mobile_agent/packs/threads.py +97 -0
  644. mobile_agent/packs/tiktok.py +455 -0
  645. mobile_agent/packs/tools.py +290 -0
  646. mobile_agent/packs/x.py +127 -0
  647. mobile_agent/packs/youtube.py +199 -0
  648. mobile_agent/parity.py +270 -0
  649. mobile_agent/paths.py +99 -0
  650. mobile_agent/perception.py +470 -0
  651. mobile_agent/permissions/__init__.py +32 -0
  652. mobile_agent/permissions/consequence.py +535 -0
  653. mobile_agent/permissions/exceptions.py +99 -0
  654. mobile_agent/permissions/gate.py +316 -0
  655. mobile_agent/permissions/outside.py +316 -0
  656. mobile_agent/permissions/policy.py +288 -0
  657. mobile_agent/permissions/project_policy.py +411 -0
  658. mobile_agent/permissions/register.py +148 -0
  659. mobile_agent/permissions/rules.py +291 -0
  660. mobile_agent/phone_io/__init__.py +146 -0
  661. mobile_agent/phone_io/cli.py +427 -0
  662. mobile_agent/phone_io/clipboard.py +78 -0
  663. mobile_agent/phone_io/controls.py +78 -0
  664. mobile_agent/phone_io/files.py +289 -0
  665. mobile_agent/phone_io/handoff.py +123 -0
  666. mobile_agent/phone_io/icloud.py +118 -0
  667. mobile_agent/phone_io/install.py +188 -0
  668. mobile_agent/phone_io/network_capture.py +188 -0
  669. mobile_agent/phone_io/network_cli.py +138 -0
  670. mobile_agent/phone_io/network_profiles.py +138 -0
  671. mobile_agent/phone_io/photos.py +512 -0
  672. mobile_agent/phone_io/recording.py +115 -0
  673. mobile_agent/phone_io/secrets.py +146 -0
  674. mobile_agent/phone_io/transfer.py +895 -0
  675. mobile_agent/phone_io/tunnel.py +129 -0
  676. mobile_agent/phone_ready.py +91 -0
  677. mobile_agent/pinning.py +158 -0
  678. mobile_agent/plan_gate.py +807 -0
  679. mobile_agent/plugins/__init__.py +3 -0
  680. mobile_agent/plugins/install.py +163 -0
  681. mobile_agent/plugins/manifest.py +242 -0
  682. mobile_agent/plugins/register.py +15 -0
  683. mobile_agent/plugins/routes.py +82 -0
  684. mobile_agent/plugins/service.py +322 -0
  685. mobile_agent/plugins/skills.py +76 -0
  686. mobile_agent/plugins/store.py +79 -0
  687. mobile_agent/pool.py +285 -0
  688. mobile_agent/power_watch.py +275 -0
  689. mobile_agent/premium_model.py +22 -0
  690. mobile_agent/project_env.py +291 -0
  691. mobile_agent/project_env_cli.py +74 -0
  692. mobile_agent/project_env_routes.py +55 -0
  693. mobile_agent/projects/__init__.py +27 -0
  694. mobile_agent/projects/ci.py +332 -0
  695. mobile_agent/projects/clone.py +339 -0
  696. mobile_agent/projects/drafts.py +158 -0
  697. mobile_agent/projects/folder.py +436 -0
  698. mobile_agent/projects/github.py +131 -0
  699. mobile_agent/projects/register.py +31 -0
  700. mobile_agent/projects/revisions.py +253 -0
  701. mobile_agent/projects/routes.py +234 -0
  702. mobile_agent/projects/runs.py +297 -0
  703. mobile_agent/projects/service.py +695 -0
  704. mobile_agent/projects/setup.py +84 -0
  705. mobile_agent/projects/store.py +379 -0
  706. mobile_agent/projects/sync.py +380 -0
  707. mobile_agent/projects/workflow_files.py +270 -0
  708. mobile_agent/replay_core/__init__.py +26 -0
  709. mobile_agent/replay_core/checks.py +594 -0
  710. mobile_agent/replay_core/engine.py +596 -0
  711. mobile_agent/replay_core/heal.py +364 -0
  712. mobile_agent/replay_core/path.py +240 -0
  713. mobile_agent/replay_core/recorder.py +206 -0
  714. mobile_agent/replay_core/store.py +171 -0
  715. mobile_agent/result_blocks.py +941 -0
  716. mobile_agent/run.py +642 -0
  717. mobile_agent/run_delivery.py +123 -0
  718. mobile_agent/run_executor.py +200 -0
  719. mobile_agent/run_export.py +284 -0
  720. mobile_agent/run_gif.py +891 -0
  721. mobile_agent/runner_hints.py +86 -0
  722. mobile_agent/runner_renewal.py +205 -0
  723. mobile_agent/runtime_create.py +549 -0
  724. mobile_agent/runtime_driver.py +458 -0
  725. mobile_agent/runtime_limits.py +319 -0
  726. mobile_agent/runtime_queue.py +394 -0
  727. mobile_agent/runtime_runs.py +383 -0
  728. mobile_agent/runtime_status.py +181 -0
  729. mobile_agent/runtime_teach.py +295 -0
  730. mobile_agent/runtime_work.py +599 -0
  731. mobile_agent/screen_text.py +101 -0
  732. mobile_agent/secret_filter.py +113 -0
  733. mobile_agent/serve.py +160 -0
  734. mobile_agent/server.py +363 -0
  735. mobile_agent/service_support.py +42 -0
  736. mobile_agent/settle/__init__.py +1 -0
  737. mobile_agent/settle/predict.py +102 -0
  738. mobile_agent/settle/replay.py +92 -0
  739. mobile_agent/settle/store.py +93 -0
  740. mobile_agent/settle/stream.py +43 -0
  741. mobile_agent/setup_cli.py +225 -0
  742. mobile_agent/setup_errors.py +87 -0
  743. mobile_agent/setup_service.py +527 -0
  744. mobile_agent/share.py +104 -0
  745. mobile_agent/signing.py +184 -0
  746. mobile_agent/sim/__init__.py +6 -0
  747. mobile_agent/sim/__main__.py +26 -0
  748. mobile_agent/sim/api.py +952 -0
  749. mobile_agent/sim/apps.py +172 -0
  750. mobile_agent/sim/biometrics.py +84 -0
  751. mobile_agent/sim/cli.py +271 -0
  752. mobile_agent/sim/doctor.py +293 -0
  753. mobile_agent/sim/governor.py +90 -0
  754. mobile_agent/sim/holders.py +88 -0
  755. mobile_agent/sim/models.py +62 -0
  756. mobile_agent/sim/registry.py +161 -0
  757. mobile_agent/sim/simctl.py +269 -0
  758. mobile_agent/sim/wda.py +618 -0
  759. mobile_agent/sim/xcode.py +74 -0
  760. mobile_agent/skills/__init__.py +12 -0
  761. mobile_agent/skills/codes.py +597 -0
  762. mobile_agent/skills/notifications.py +281 -0
  763. mobile_agent/skills/open_url.py +127 -0
  764. mobile_agent/smart_events.py +588 -0
  765. mobile_agent/smart_result.py +490 -0
  766. mobile_agent/smart_run.py +413 -0
  767. mobile_agent/state.py +662 -0
  768. mobile_agent/states.py +217 -0
  769. mobile_agent/steering.py +70 -0
  770. mobile_agent/style.py +178 -0
  771. mobile_agent/supervised.py +234 -0
  772. mobile_agent/swift/helper_bt_hid.swift +534 -0
  773. mobile_agent/swift/helper_doc_text.swift +184 -0
  774. mobile_agent/swift/helper_draft_reply.swift +170 -0
  775. mobile_agent/swift/helper_map.swift +110 -0
  776. mobile_agent/swift/helper_mirroring.swift +1099 -0
  777. mobile_agent/swift/helper_mjpeg_mp4.swift +103 -0
  778. mobile_agent/swift/helper_pdf.swift +100 -0
  779. mobile_agent/swift/helper_phone_audio.swift +993 -0
  780. mobile_agent/swift/helper_phone_screen.swift +416 -0
  781. mobile_agent/swift/helper_screen_text.swift +24 -0
  782. mobile_agent/swift/helper_transfer.swift +246 -0
  783. mobile_agent/swift/ocr.swift +22 -0
  784. mobile_agent/task_policy.py +337 -0
  785. mobile_agent/team_signing/__init__.py +34 -0
  786. mobile_agent/team_signing/apple_ca.py +37 -0
  787. mobile_agent/team_signing/bundle.py +136 -0
  788. mobile_agent/team_signing/cli.py +208 -0
  789. mobile_agent/team_signing/identity.py +248 -0
  790. mobile_agent/team_signing/ledger.py +105 -0
  791. mobile_agent/team_signing/prebuilt.py +61 -0
  792. mobile_agent/team_signing/resign.py +256 -0
  793. mobile_agent/team_signing/service.py +382 -0
  794. mobile_agent/team_signing/tooling.py +243 -0
  795. mobile_agent/team_signing/writes.py +268 -0
  796. mobile_agent/terminal_image.py +81 -0
  797. mobile_agent/testkit/__init__.py +1 -0
  798. mobile_agent/testkit/a11y_baseline.py +132 -0
  799. mobile_agent/testkit/api.py +403 -0
  800. mobile_agent/testkit/capsule.py +400 -0
  801. mobile_agent/testkit/capsule_cli.py +57 -0
  802. mobile_agent/testkit/cli.py +948 -0
  803. mobile_agent/testkit/cli_arguments.py +193 -0
  804. mobile_agent/testkit/comparison.py +288 -0
  805. mobile_agent/testkit/comparison_cli.py +41 -0
  806. mobile_agent/testkit/discover.py +257 -0
  807. mobile_agent/testkit/editor.py +243 -0
  808. mobile_agent/testkit/execute.py +515 -0
  809. mobile_agent/testkit/github_checks.py +248 -0
  810. mobile_agent/testkit/gitref.py +66 -0
  811. mobile_agent/testkit/heal_review.py +176 -0
  812. mobile_agent/testkit/health.py +480 -0
  813. mobile_agent/testkit/history.py +255 -0
  814. mobile_agent/testkit/hosted_results.py +321 -0
  815. mobile_agent/testkit/input_files.py +173 -0
  816. mobile_agent/testkit/jobs.py +285 -0
  817. mobile_agent/testkit/junit_verdict.py +33 -0
  818. mobile_agent/testkit/maestro.py +802 -0
  819. mobile_agent/testkit/mcp.py +562 -0
  820. mobile_agent/testkit/mcp_comparison.py +88 -0
  821. mobile_agent/testkit/minimize.py +471 -0
  822. mobile_agent/testkit/minimizer_cli.py +48 -0
  823. mobile_agent/testkit/notify.py +212 -0
  824. mobile_agent/testkit/phone_video.py +191 -0
  825. mobile_agent/testkit/pr.py +491 -0
  826. mobile_agent/testkit/pr_report.py +562 -0
  827. mobile_agent/testkit/prbuild.py +148 -0
  828. mobile_agent/testkit/proof_report.py +55 -0
  829. mobile_agent/testkit/provenance.py +119 -0
  830. mobile_agent/testkit/prrun.py +244 -0
  831. mobile_agent/testkit/recent.py +166 -0
  832. mobile_agent/testkit/recipes.py +146 -0
  833. mobile_agent/testkit/record.py +204 -0
  834. mobile_agent/testkit/register.py +288 -0
  835. mobile_agent/testkit/reports.py +998 -0
  836. mobile_agent/testkit/repro.py +432 -0
  837. mobile_agent/testkit/scenario.py +440 -0
  838. mobile_agent/testkit/scenario_cli.py +37 -0
  839. mobile_agent/testkit/snapshots_cli.py +60 -0
  840. mobile_agent/testkit/suite.py +670 -0
  841. mobile_agent/testkit/targets.py +497 -0
  842. mobile_agent/testkit/xctest.py +378 -0
  843. mobile_agent/threads/__init__.py +1 -0
  844. mobile_agent/threads/chat_once.py +264 -0
  845. mobile_agent/threads/cli.py +740 -0
  846. mobile_agent/threads/client.py +326 -0
  847. mobile_agent/threads/context.py +246 -0
  848. mobile_agent/threads/mcp.py +739 -0
  849. mobile_agent/threads/recent.py +34 -0
  850. mobile_agent/threads/register.py +54 -0
  851. mobile_agent/threads/routes.py +188 -0
  852. mobile_agent/threads/service.py +975 -0
  853. mobile_agent/threads/store.py +361 -0
  854. mobile_agent/threads/summary.py +117 -0
  855. mobile_agent/threads/term.py +101 -0
  856. mobile_agent/threads/workflow_cli.py +104 -0
  857. mobile_agent/tls.py +36 -0
  858. mobile_agent/traces.py +271 -0
  859. mobile_agent/tracks.py +235 -0
  860. mobile_agent/transport.py +271 -0
  861. mobile_agent/triggers/__init__.py +1 -0
  862. mobile_agent/triggers/actions.py +204 -0
  863. mobile_agent/triggers/model.py +146 -0
  864. mobile_agent/triggers/register.py +29 -0
  865. mobile_agent/triggers/routes.py +101 -0
  866. mobile_agent/triggers/service.py +387 -0
  867. mobile_agent/triggers/store.py +142 -0
  868. mobile_agent/tui/__init__.py +89 -0
  869. mobile_agent/tui/app.py +582 -0
  870. mobile_agent/tui/chrome.py +321 -0
  871. mobile_agent/tui/commands.py +375 -0
  872. mobile_agent/tui/demo_phone.py +369 -0
  873. mobile_agent/tui/images.py +29 -0
  874. mobile_agent/tui/keys.py +64 -0
  875. mobile_agent/tui/prompt.py +195 -0
  876. mobile_agent/tui/render.py +395 -0
  877. mobile_agent/tui/screens.py +180 -0
  878. mobile_agent/tui/session.py +308 -0
  879. mobile_agent/tui/tasks.py +287 -0
  880. mobile_agent/tui/theme.py +118 -0
  881. mobile_agent/tui/widgets.py +272 -0
  882. mobile_agent/update_check.py +213 -0
  883. mobile_agent/usage.py +184 -0
  884. mobile_agent/verify/__init__.py +0 -0
  885. mobile_agent/verify/__main__.py +41 -0
  886. mobile_agent/verify/a11y.py +189 -0
  887. mobile_agent/verify/actions.py +290 -0
  888. mobile_agent/verify/assertions.py +570 -0
  889. mobile_agent/verify/audit.py +312 -0
  890. mobile_agent/verify/business.py +221 -0
  891. mobile_agent/verify/business_run.py +131 -0
  892. mobile_agent/verify/business_state.py +168 -0
  893. mobile_agent/verify/check_schema.py +398 -0
  894. mobile_agent/verify/checks.py +614 -0
  895. mobile_agent/verify/cli.py +423 -0
  896. mobile_agent/verify/conditions.py +483 -0
  897. mobile_agent/verify/crashes.py +321 -0
  898. mobile_agent/verify/device_audit.py +246 -0
  899. mobile_agent/verify/dialogs.py +287 -0
  900. mobile_agent/verify/evidence.py +705 -0
  901. mobile_agent/verify/fixtures.py +305 -0
  902. mobile_agent/verify/flow.py +213 -0
  903. mobile_agent/verify/l10n.py +141 -0
  904. mobile_agent/verify/netcerts.py +67 -0
  905. mobile_agent/verify/netproxy.py +356 -0
  906. mobile_agent/verify/netrun.py +81 -0
  907. mobile_agent/verify/netshim.m +28 -0
  908. mobile_agent/verify/netshim.py +48 -0
  909. mobile_agent/verify/netstubs.py +132 -0
  910. mobile_agent/verify/overlay.py +174 -0
  911. mobile_agent/verify/perf.py +239 -0
  912. mobile_agent/verify/proof.py +192 -0
  913. mobile_agent/verify/replay_admission.py +48 -0
  914. mobile_agent/verify/report.py +952 -0
  915. mobile_agent/verify/run_cleanup.py +67 -0
  916. mobile_agent/verify/run_links.py +48 -0
  917. mobile_agent/verify/run_perf.py +44 -0
  918. mobile_agent/verify/run_secrets.py +39 -0
  919. mobile_agent/verify/runner.py +976 -0
  920. mobile_agent/verify/runs.py +57 -0
  921. mobile_agent/verify/screen_coverage.py +181 -0
  922. mobile_agent/verify/smart.py +106 -0
  923. mobile_agent/verify/snapshot_run.py +114 -0
  924. mobile_agent/verify/snapshots.py +306 -0
  925. mobile_agent/verify/sound.py +335 -0
  926. mobile_agent/verify/source_observation.py +130 -0
  927. mobile_agent/verify/system_surfaces/__init__.py +7 -0
  928. mobile_agent/verify/system_surfaces/play.py +106 -0
  929. mobile_agent/verify/system_surfaces/spec.py +139 -0
  930. mobile_agent/verify/system_surfaces/stage.py +138 -0
  931. mobile_agent/verify/system_surfaces/surfaces.py +121 -0
  932. mobile_agent/verify/tree.py +223 -0
  933. mobile_agent/verify/verdict.py +377 -0
  934. mobile_agent/verify/wire.py +392 -0
  935. mobile_agent/verify/wirestore.py +176 -0
  936. mobile_agent/waiting/__init__.py +1 -0
  937. mobile_agent/waiting/contacts.py +73 -0
  938. mobile_agent/waiting/drafts.py +136 -0
  939. mobile_agent/waiting/job.py +145 -0
  940. mobile_agent/waiting/messages.py +174 -0
  941. mobile_agent/waiting/rank.py +115 -0
  942. mobile_agent/waiting/register.py +26 -0
  943. mobile_agent/watch.py +502 -0
  944. mobile_agent/wda/__init__.py +2 -0
  945. mobile_agent/wda/frame_clock.py +646 -0
  946. mobile_agent/wda/frame_source.py +325 -0
  947. mobile_agent/wda/frame_store.py +115 -0
  948. mobile_agent/wda/gate.py +163 -0
  949. mobile_agent/wda/gestures.py +291 -0
  950. mobile_agent/wda/guards.py +94 -0
  951. mobile_agent/wda/identity.py +55 -0
  952. mobile_agent/wda/observe.py +884 -0
  953. mobile_agent/wda/patches.py +856 -0
  954. mobile_agent/wda/point_gestures.py +150 -0
  955. mobile_agent/wda/source.py +32 -0
  956. mobile_agent/wda/system_dialogs.py +571 -0
  957. mobile_agent/wda/text.py +694 -0
  958. mobile_agent/wda/timeouts.py +77 -0
  959. mobile_agent/wda/video.py +505 -0
  960. mobile_agent/wda/watchdog.py +243 -0
  961. mobile_agent/wireless/__init__.py +15 -0
  962. mobile_agent/wireless/api.py +354 -0
  963. mobile_agent/wireless/cli.py +287 -0
  964. mobile_agent/wireless/devicectl.py +229 -0
  965. mobile_agent/wireless/exposure.py +33 -0
  966. mobile_agent/wireless/mobster-wifi-pair.c +122 -0
  967. mobile_agent/wireless/monitor.py +332 -0
  968. mobile_agent/wireless/network.py +334 -0
  969. mobile_agent/wireless/onboarding.py +293 -0
  970. mobile_agent/wireless/pair.py +124 -0
  971. mobile_agent/wireless/probe.py +322 -0
  972. mobile_agent/wireless/register.py +8 -0
  973. mobile_agent/wireless/relay.py +179 -0
  974. mobile_agent/wireless/result.py +87 -0
  975. mobile_agent/wireless/store.py +85 -0
  976. mobile_agent/wireless/switch.py +77 -0
  977. mobile_agent/wireless/transport.py +118 -0
  978. mobile_agent/wireless/words.py +81 -0
  979. mobile_agent/wireless/xctestrun.py +83 -0
  980. mobile_agent/workflow_tools.py +59 -0
  981. mobile_agent/workflows/__init__.py +2 -0
  982. mobile_agent/workflows/brief.py +201 -0
  983. mobile_agent/workflows/cron.py +110 -0
  984. mobile_agent/workflows/health.py +257 -0
  985. mobile_agent/workflows/history.py +126 -0
  986. mobile_agent/workflows/records.py +442 -0
  987. mobile_agent/workflows/runs.py +1000 -0
  988. mobile_agent/workflows/scheduler.py +134 -0
  989. mobile_agent/workflows/service.py +973 -0
  990. mobile_agent/workflows/starters.py +285 -0
  991. mobile_agent/workflows/triggers.py +248 -0
  992. mobile_agent/xray/__init__.py +1 -0
  993. mobile_agent/xray/app_cli.py +116 -0
  994. mobile_agent/xray/cli.py +21 -0
  995. mobile_agent/xray/crawl.py +754 -0
  996. mobile_agent/xray/diff.py +171 -0
  997. mobile_agent/xray/diff_cli.py +43 -0
  998. mobile_agent/xray/job.py +184 -0
  999. mobile_agent/xray/paywall.py +188 -0
  1000. mobile_agent/xray/policy.py +305 -0
  1001. mobile_agent/xray/register.py +43 -0
  1002. mobile_agent/xray/report.py +283 -0
  1003. mobile_agent/xray/screens.py +117 -0
  1004. mobster_cli-0.4.3.dist-info/METADATA +93 -0
  1005. mobster_cli-0.4.3.dist-info/RECORD +1009 -0
  1006. mobster_cli-0.4.3.dist-info/WHEEL +5 -0
  1007. mobster_cli-0.4.3.dist-info/entry_points.txt +3 -0
  1008. mobster_cli-0.4.3.dist-info/licenses/LICENSE +21 -0
  1009. mobster_cli-0.4.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,978 @@
1
+ """The USB iPhone lifecycle, driven from the setup flow."""
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ import shutil
7
+ import signal
8
+ import subprocess
9
+ import threading
10
+ import time
11
+ from pathlib import Path
12
+
13
+ from . import env_file, mac_tools, owner_presence, setup_errors, signing, supervised
14
+ from .device_info import MODEL_NAMES, enclosure_code
15
+ from .devices import LOOPBACK, UDID_PATTERN
16
+ from .mac_tools import VIDEO_PORT, WDA_PORT, XCODE_OK_TTL, XCODE_RETRY_TTL, XCODE_SHIMS
17
+ from .owner_presence import OwnerPresence, Poller
18
+ from .runner_hints import (
19
+ AUTOMATION_WAIT_HINT,
20
+ AUTOMATION_WAIT_S,
21
+ AUTOMATION_WAITING,
22
+ EXPIRED_HINT,
23
+ EXPOSED_HINT,
24
+ LOCKED_START_HINT,
25
+ PREARM_ASK,
26
+ PREARM_MIN_IOS,
27
+ RUNNER_HINTS,
28
+ build_hint,
29
+ runner_stamp,
30
+ )
31
+ from .supervised import BUILD_STEPS, stop_group
32
+ from .transport import read_kept
33
+ from .wda import gate as wda_gate
34
+ from .wda import patches as wda_patches
35
+ from .wda import source as wda_source
36
+ from .wda.patches import BUNDLE_REBRAND, LEGACY_RUNNER, RUNNER_ID
37
+ from .wda.watchdog import Watchdog
38
+
39
+ TEAMS_TTL = 60.0
40
+ USB_TTL = 5.0
41
+ TEAM_PATTERN = re.compile(r"[A-Z0-9]{10}")
42
+ WATCH_INTERVAL = 5.0
43
+ # WDA can wedge: /status still answers while every call that needs the phone's UI
44
+ # (screen reads, taps, video) hangs, e.g. with the phone locked or behind Control
45
+ # Center or the iOS 26 Siri glow (measured 2026-09-23: reads hung for minutes).
46
+ # /wda/locked is cheap when WDA is healthy, so it is the probe.
47
+ PROBE_INTERVAL = 3.0
48
+ # The watch loop looks at the probe this often; its other jobs (autostart, presence, exposure, a locked build's retry,
49
+ # the renewal) still run every WATCH_INTERVAL. A cause is named within engine_health.NAMED_WITHIN_S of when it begins:
50
+ # PROBE_TICK + PROBE_INTERVAL + a probe's time + the sampler's tick.
51
+ PROBE_TICK = 1.0
52
+ PROBE_TIMEOUT = 3.0
53
+ # A runner this soon after its start is still starting, not wedged (xcodebuild takes a while to answer /status).
54
+ STARTUP_GRACE_SECONDS = 120.0
55
+ # WEDGED_RESTART_SECONDS and MIN_RESTART_GAP live in wda/watchdog.py, which decides the restart.
56
+ LOG_TAIL = 40
57
+ # How long one CoreDevice lock read may take before the runner launches as it would without one.
58
+ HOLD_READ_TIMEOUT = 4.0
59
+ RUNNER_LOG_WINDOW = 16384
60
+ AUTOMATION_OK_KEPT = 32 # phones remembered as having answered once (prearm stops asking for them)
61
+
62
+
63
+ def wifi_runner_command(xcodebuild, plan, udid):
64
+ if not isinstance(udid, str) or not UDID_PATTERN.fullmatch(udid):
65
+ raise ValueError("A UDID is 24 to 40 hex digits and dashes")
66
+ return [xcodebuild, "test-without-building", "-xctestrun", str(plan), "-destination", f"id={udid}"]
67
+
68
+
69
+ # One build at a time across phones: every build rebrands and patches the shared WebDriverAgent clone, and
70
+ # two xcodebuilds at once would also double the heaviest job Mobster runs.
71
+ BUILD_LOCK = threading.Lock()
72
+
73
+
74
+ class DeviceManager:
75
+ """One USB iPhone's lifecycle."""
76
+
77
+ build_step = None # the git or xcodebuild process the build is running, if any
78
+
79
+ def __init__(self, data_dir, wda_url=f"http://127.0.0.1:{WDA_PORT}", *, udid=None, root=None,
80
+ video_port=VIDEO_PORT):
81
+ self.data_dir = Path(data_dir)
82
+ try:
83
+ env_file.private_dir(self.data_dir) # keys, journal, team id and logs live here
84
+ except OSError:
85
+ pass
86
+ self.wda_url = wda_url.rstrip("/")
87
+ from urllib.parse import urlsplit
88
+ self.wda_port = urlsplit(self.wda_url).port or WDA_PORT
89
+ self.video_port = video_port
90
+ self.pinned = udid
91
+ home = Path(root) if root is not None else self.data_dir
92
+ self.project = self.data_dir / "WebDriverAgent"
93
+ self.derived = home / "wda-build"
94
+ self.logs = home / "logs"
95
+ self.settings_path = home / "device.json"
96
+ self.lock = threading.Lock()
97
+ self.build = {"state": "idle", "error": None, "log_tail": []}
98
+ self.runner_log_start = 0
99
+ self.watching = threading.Event() # set to stop watch()
100
+ self.health = {"responsive": None, "locked": None, "since": None, "probed_at": 0.0, "ready": None}
101
+ # Whether a task runs on this phone (SetupService sets it) and how a task ends when its helper is dead.
102
+ self.busy = lambda: False
103
+ self.end_task = None
104
+ self.idle_hooks = [] # called once when the phone has been idle for owner_presence.IDLE_RELEASE_S
105
+ self.runner_started_at = None
106
+ self.watchdog = Watchdog(self._restart_wedged_runner, url=lambda: self.wda_url, busy=lambda: self.busy(),
107
+ alive=self._runner_alive,
108
+ on_dead_runner=lambda message: self.end_task(message) if callable(self.end_task)
109
+ else None)
110
+ # Whether the phone's owner is using it (owner_presence.py); polled by watch() while no task holds the phone.
111
+ self.presence = OwnerPresence()
112
+ self.presence_poller = Poller(self.presence, self.wda_url, busy=lambda: self.busy())
113
+ owner_presence.register(self.wda_url, self.presence)
114
+ # One start or stop at a time: the watcher and the setup flow can both start the runner.
115
+ self.runner_lock = threading.RLock()
116
+ self.runner = None
117
+ self.relay = None
118
+ # What the runner started last uses: "usb" (iproxy), "wifi" (the tunnel and wireless.relay) or None (stopped).
119
+ # Only wireless/monitor.py starts it over Wi-Fi, with MOBSTER_WIFI_TRANSPORT on.
120
+ self.transport = None
121
+ self.wifi_address = None
122
+ # The last start the exposure check (check_exposure) looked at, and what answered where it must not.
123
+ self.exposure_checked = None
124
+ self.exposure = None
125
+ self.runner_line = self.runner_cause = None
126
+ self._answered = set() # phones whose helper answered (note_answered), so prearm reads no file
127
+ # whether the chosen phone was on the cable at the last watch round (rearm_on_attach)
128
+ self._attached_before = None
129
+ self.locked_retry_for = None # the started_at of the locked-phone build failure already tried again
130
+ self.cache = {}
131
+ # Rebuilds the runner before its signature runs out (runner_renewal.RunnerRenewal); set by SetupService.
132
+ self.renewal = None
133
+ # Phones other managers drive (devices.py): the primary never picks one as "the only phone connected".
134
+ self.claimed = lambda: ()
135
+ if udid is not None and self.settings_path.parent.exists() and self.settings().get("udid") != udid:
136
+ try:
137
+ self.save_settings(udid=udid)
138
+ except OSError:
139
+ pass
140
+
141
+ # -- inspection ----------------------------------------------------------
142
+
143
+ def settings(self):
144
+ try:
145
+ value = json.loads(self.settings_path.read_text())
146
+ return value if isinstance(value, dict) else {}
147
+ except (OSError, ValueError):
148
+ return {}
149
+
150
+ def save_settings(self, **changes):
151
+ settings = {**self.settings(), **changes}
152
+ env_file.private_dir(self.data_dir)
153
+ env_file.private_dir(self.settings_path.parent)
154
+ with env_file.open_private(self.settings_path, "w") as stream:
155
+ stream.write(json.dumps(settings, indent=1))
156
+ return settings
157
+
158
+ def tools(self):
159
+ """Paths of the tools setup needs (None when missing or unusable),
160
+ and Xcode's state (mac_tools.xcode_status)."""
161
+ cached = self.cache.get("xcode")
162
+ if cached and time.monotonic() < cached[0]:
163
+ xcode = cached[1]
164
+ else:
165
+ xcode = mac_tools.xcode_status()
166
+ self.cache["xcode"] = (time.monotonic() + (XCODE_OK_TTL if xcode["state"] == "ok" else XCODE_RETRY_TTL),
167
+ xcode)
168
+ developer = xcode["developer_dir"]
169
+ return {"xcodebuild": mac_tools.xcode_tool("xcodebuild", developer) if xcode["state"] == "ok" else None,
170
+ "iproxy": mac_tools.tool("iproxy"), "idevice_id": mac_tools.tool("idevice_id"),
171
+ "ideviceinfo": mac_tools.tool("ideviceinfo"),
172
+ "git": mac_tools.xcode_tool("git", developer), "xcode": xcode}
173
+
174
+ def refresh(self):
175
+ """Forget the cached tool and team checks, so the next read sees a fix the user just made."""
176
+ for key in ("xcode", "teams", "usb"):
177
+ self.cache.pop(key, None)
178
+
179
+ def xcode_env(self):
180
+ developer = self.tools()["xcode"]["developer_dir"]
181
+ return mac_tools.xcode_env(developer) if developer else None
182
+
183
+ def runner_env(self):
184
+ return {**(self.xcode_env() or mac_tools.xcode_env()), **wda_gate.runner_env("phone")}
185
+
186
+ def devices(self):
187
+ """USB iPhones, with whether this Mac is trusted (lockdown info readable)."""
188
+ listing = mac_tools.tool("idevice_id")
189
+ output = mac_tools.run([listing, "-l"]) if listing else None
190
+ udids = []
191
+ for line in (output or "").splitlines():
192
+ udid = line.strip()
193
+ # 0000FE01-… entries are not physical USB phones.
194
+ if UDID_PATTERN.fullmatch(udid) and not udid.startswith("0000FE01") and udid not in udids:
195
+ udids.append(udid)
196
+ info_tool = mac_tools.tool("ideviceinfo")
197
+ devices = []
198
+ for udid in udids:
199
+ # A trusted phone's identity is stable; untrusted ones are re-read so Trust shows at once.
200
+ cached = self.cache.get(("info", udid))
201
+ info = dict(cached[1]) if cached and time.monotonic() - cached[0] < 60 else {}
202
+ if info_tool and not info:
203
+ for key, field in (("DeviceName", "name"), ("ProductType", "model"), ("ProductVersion", "ios"),
204
+ ("DeviceEnclosureColor", "enclosure")):
205
+ value = mac_tools.run([info_tool, "-u", udid, "-k", key], timeout=5)
206
+ if value is not None:
207
+ info[field] = value.strip()
208
+ if info:
209
+ self.cache[("info", udid)] = (time.monotonic(), dict(info))
210
+ devices.append({"udid": udid, "name": info.get("name") or "iPhone", "model": info.get("model"),
211
+ "modelName": MODEL_NAMES.get(info.get("model")), "ios": info.get("ios"),
212
+ "enclosure": enclosure_code(info.get("enclosure")), "trusted": bool(info)})
213
+ return devices
214
+
215
+ def usb_iphones(self):
216
+ cached = self.cache.get("usb")
217
+ if cached and time.monotonic() - cached[0] < USB_TTL:
218
+ return cached[1]
219
+ count = None
220
+ for data_type in ("SPUSBHostDataType", "SPUSBDataType"):
221
+ output = mac_tools.run(["/usr/sbin/system_profiler", data_type, "-json"], timeout=8)
222
+ try:
223
+ tree = json.loads(output) if output else None
224
+ except ValueError:
225
+ tree = None
226
+ if isinstance(tree, dict) and isinstance(tree.get(data_type), list):
227
+ count = mac_tools.count_iphones(tree[data_type])
228
+ break
229
+ self.cache["usb"] = (time.monotonic(), count)
230
+ return count
231
+
232
+ def device(self, devices=None):
233
+ # never a different phone than the one chosen: a runner signed for one phone fails on another
234
+ devices = self.devices() if devices is None else devices
235
+ chosen = self.pinned or self.settings().get("udid")
236
+ if chosen:
237
+ return next((d for d in devices if d["udid"] == chosen), None)
238
+ claimed = set(self.claimed())
239
+ free = [d for d in devices if d["udid"] not in claimed]
240
+ return free[0] if len(free) == 1 else None
241
+
242
+ def choose(self, udid):
243
+ if not isinstance(udid, str) or not UDID_PATTERN.fullmatch(udid):
244
+ raise ValueError("Choose a connected iPhone")
245
+ if self.pinned and udid != self.pinned:
246
+ raise LookupError("This device is one iPhone; set up another iPhone as its own device")
247
+ if udid in set(self.claimed()):
248
+ raise LookupError("That iPhone is already set up as another device. Choose a different iPhone.")
249
+ if not any(d["udid"] == udid for d in self.devices()):
250
+ raise LookupError("That iPhone is not connected")
251
+ name = next(d["name"] for d in self.devices() if d["udid"] == udid)
252
+ if udid != self.settings().get("udid"):
253
+ self.stop_runner()
254
+ self.save_settings(udid=udid, device_name=name)
255
+
256
+ def teams(self):
257
+ if "teams" in self.cache and time.monotonic() - self.cache["teams"][0] < TEAMS_TTL:
258
+ return self.cache["teams"][1]
259
+ teams = signing.merge_teams(signing.account_teams(), signing.certificate_teams())
260
+ self.cache["teams"] = (time.monotonic(), teams)
261
+ return teams
262
+
263
+ def driver(self):
264
+ """What installs and launches the runner for this phone: "xcodebuild" (Xcode signs and runs it), or, when it was
265
+ signed on this Mac with the team's profile (team_signing), "pymobiledevice3" or "go-ios"."""
266
+ return self.settings().get("driver") or "xcodebuild"
267
+
268
+ def built(self):
269
+ """Built for the chosen phone (a build records the phone it was made for), and still signed."""
270
+ settings = self.settings()
271
+ built_for = settings.get("built_for", settings.get("udid"))
272
+ if self.driver() != "xcodebuild":
273
+ runner = (settings.get("signed_runner") or {}).get("path")
274
+ return bool(built_for) and built_for == settings.get("udid") and bool(runner) and Path(runner).is_dir() \
275
+ and not self.expired() and self.patched()
276
+ return bool(built_for) and built_for == settings.get("udid") and any(
277
+ self.derived.glob("Build/Products/*.xctestrun")) and not self.expired() and self.patched()
278
+
279
+ def patched(self):
280
+ """Whether the build was made with the WDA patches this Mobster has (wda_patches.patch_digest),
281
+ not an older set."""
282
+ return self.settings().get("built_patches") == wda_patches.patch_digest()
283
+
284
+ def out_of_date(self):
285
+ """A build is there, made with other WDA patches than this Mobster has: it has to be built again."""
286
+ return any(self.derived.glob("Build/Products/*.xctestrun")) and not self.patched()
287
+
288
+ def signature_expiry(self):
289
+ settings = self.settings()
290
+ if "expires_at" not in settings and any(self.derived.glob(signing.PROFILE_GLOB)):
291
+ settings = self.save_settings(expires_at=signing.profile_expiry(self.derived))
292
+ value = settings.get("expires_at")
293
+ return value if isinstance(value, (int, float)) and not isinstance(value, bool) else None
294
+
295
+ def expired(self, now=None):
296
+ expires = self.signature_expiry()
297
+ return expires is not None and (time.time() if now is None else now) >= expires
298
+
299
+ def _wda_get(self, path, timeout):
300
+ # one kept connection: a connection per read ran WDA out of descriptors
301
+ return read_kept(self.wda_url, path, timeout)
302
+
303
+ def wda_ready(self, timeout=1.5):
304
+ try:
305
+ return (self._wda_get("/status", timeout).get("value") or {}).get("ready") is True
306
+ except Exception:
307
+ return False
308
+
309
+ def _runner_alive(self):
310
+ """Whether the runner's child process runs now (not just its supervisor thread)."""
311
+ runner = self.runner
312
+ alive = getattr(runner, "alive", None)
313
+ return bool(alive) if alive is not None else bool(runner is not None and runner.running)
314
+
315
+ def runner_waiting(self):
316
+ """"unlock" while the runner is held back because the phone is locked (``_runner_hold``), else None."""
317
+ runner = self.runner
318
+ return "unlock" if getattr(runner, "held", None) == "locked" and not self._runner_alive() else None
319
+
320
+ def _runner_hold(self):
321
+ # xcodebuild on a locked phone waits 60 s and fails, and each failure counts toward 'didn't start'
322
+ from .wireless import devicectl
323
+ udid = self.pinned or self.settings().get("udid")
324
+ return "locked" if udid and devicectl.read_lock_state(udid, timeout=HOLD_READ_TIMEOUT) is True else None
325
+
326
+ def retry_locked_build(self):
327
+ build = self.build
328
+ problem = build.get("problem") or {}
329
+ key = build.get("started_at")
330
+ if build.get("state") != "failed" or problem.get("kind") != "locked" or key is None \
331
+ or key == self.locked_retry_for:
332
+ return False
333
+ settings = self.settings()
334
+ udid, team = self.pinned or settings.get("udid"), build.get("team") or settings.get("team")
335
+ if not udid or not team or self.device() is None:
336
+ return False
337
+ from .wireless import devicectl
338
+ if devicectl.read_lock_state(udid, timeout=HOLD_READ_TIMEOUT) is not False:
339
+ return False
340
+ self.locked_retry_for = key
341
+ self.start_build(team)
342
+ return True
343
+
344
+ def _relaunch_gate(self):
345
+ # the watchdog admits no relaunch while an action is in flight or an outcome under a task is unknown
346
+ if self.watching.is_set():
347
+ return None
348
+ if self.watchdog.admit(time.monotonic()) is not None:
349
+ return None
350
+ return lambda: self.watchdog.done(True)
351
+
352
+ def wda_alive(self, attempts=3, timeout=4.0, gap=2.0):
353
+ # WDA serves one request at a time, so a busy runner can miss one /status; a new one would replace it mid-task
354
+ for attempt in range(attempts):
355
+ if self.wda_ready(timeout=timeout):
356
+ return True
357
+ if attempt + 1 < attempts:
358
+ time.sleep(gap)
359
+ return False
360
+
361
+ def attached(self, udid=None):
362
+ """Whether the chosen (or ``udid``'s) iPhone is plugged in now; None when that can't be told."""
363
+ return mac_tools.usb_attached(udid or self.pinned or self.settings().get("udid"))
364
+
365
+ def locked(self):
366
+ """WDA answers but the phone is locked (the last probe)."""
367
+ return self.health["locked"] is True
368
+
369
+ def health_error(self):
370
+ """What to tell the user when WDA answers but cannot reach the phone's UI."""
371
+ if self.locked():
372
+ return "Your iPhone is locked. Unlock it to run tasks and see its screen."
373
+ if self.health["responsive"] is False:
374
+ return "Your iPhone isn't responding. Unlock it and close Control Center or Siri if they're open."
375
+ return None
376
+
377
+ def prearm(self, udid=None, ios=None):
378
+ udid = udid or self.pinned or self.settings().get("udid")
379
+ if not udid or udid in self._answered or udid in (self.settings().get("automation_ok") or []):
380
+ return None
381
+ if ios is None: # from the phone list's cache (devices()): asked every second, it never runs a tool itself
382
+ cached = self.cache.get(("info", udid))
383
+ ios = cached[1].get("ios") if cached else None
384
+ try:
385
+ major = int(str(ios).split(".")[0])
386
+ except (TypeError, ValueError):
387
+ return None
388
+ return "ui_automation" if major >= PREARM_MIN_IOS else None
389
+
390
+ def note_answered(self):
391
+ udid = self.pinned or self.settings().get("udid")
392
+ if not udid or udid in self._answered:
393
+ return
394
+ kept = list(self.settings().get("automation_ok") or [])
395
+ if udid not in kept:
396
+ self.save_settings(automation_ok=(kept + [udid])[-AUTOMATION_OK_KEPT:])
397
+ self._answered.add(udid)
398
+
399
+ def runner_state(self, ready=None):
400
+ if ready is None:
401
+ ready = self.wda_ready()
402
+ if ready:
403
+ self.note_answered()
404
+ state = self._runner_state(ready)
405
+ if state["state"] != "running" and (asked := self.prearm()):
406
+ state["prearm"] = {"kind": asked, "ask": PREARM_ASK}
407
+ return state
408
+
409
+ def _runner_state(self, ready):
410
+ if self.runner is None or not self.runner.running:
411
+ # A runner started outside Mobster (scripts/usb-wda.sh, Xcode) serves just as well.
412
+ if ready:
413
+ return {"state": "running", "error": self.health_error(), "external": True}
414
+ if not self.exposure and self.settings().get("autostart") and self.built():
415
+ return {"state": "starting", "error": None} # the watch loop starts it within WATCH_INTERVAL
416
+ return {"state": "stopped", "error": EXPOSED_HINT if self.exposure else None}
417
+ if ready:
418
+ return {"state": "running", "error": self.health_error()}
419
+ if self.runner_waiting() == "unlock":
420
+ return {"state": "starting", "error": LOCKED_START_HINT, "waiting": "unlock"}
421
+ failing = self.runner.restarts >= 3 and self.runner.last_exit not in (None, 0)
422
+ hint = self.runner_hint()
423
+ return {"state": "failed" if failing else "starting",
424
+ "error": hint or ("Mobster's helper keeps stopping. Unlock your iPhone and check Developer Mode."
425
+ if failing else None)}
426
+
427
+ def runner_hint(self, now=None):
428
+ # An expired profile fails with the same "invalid code signature ... not explicitly trusted"
429
+ # sentence as an untrusted one, so the log alone would give the wrong advice.
430
+ self.runner_cause = None
431
+ if self.expired():
432
+ return EXPIRED_HINT
433
+ try:
434
+ with open(self.logs / "wda-runner.log", "rb") as log:
435
+ size = log.seek(0, os.SEEK_END)
436
+ log.seek(max(self.runner_log_start, size - RUNNER_LOG_WINDOW))
437
+ text = log.read().decode("utf-8", "replace")
438
+ except OSError:
439
+ return None
440
+ # The newest matching line wins: an unlock prompt followed by success is stale.
441
+ latest = None
442
+ self.runner_line = self.runner_cause = None
443
+ waiting_since = None # when the runner said "Running tests..." with nothing after it yet
444
+ for line in text.splitlines():
445
+ if "Running tests..." in line:
446
+ waiting_since = runner_stamp(line)
447
+ elif "Test Suite " in line or "Failed to initialize" in line:
448
+ waiting_since = None
449
+ for needle, cause, hint in RUNNER_HINTS:
450
+ if needle in line:
451
+ latest, self.runner_line, self.runner_cause = hint, line, cause
452
+ break
453
+ if "ServerURLHere" in line:
454
+ latest = self.runner_line = self.runner_cause = waiting_since = None
455
+ if waiting_since is not None and (time.time() if now is None else now) - waiting_since >= AUTOMATION_WAIT_S:
456
+ latest, self.runner_line, self.runner_cause = AUTOMATION_WAIT_HINT, AUTOMATION_WAITING, "ui_automation_off"
457
+ return latest
458
+
459
+ def runner_announced(self):
460
+ # unlike /status, which the runner before it may still answer for a few seconds after a restart
461
+ try:
462
+ with open(self.logs / "wda-runner.log", "rb") as log:
463
+ size = log.seek(0, os.SEEK_END)
464
+ log.seek(max(self.runner_log_start, size - 4 * RUNNER_LOG_WINDOW))
465
+ return b"ServerURLHere" in log.read()
466
+ except OSError:
467
+ return False
468
+
469
+ def runner_problem(self):
470
+ """The runner's current failure as a one-line fix (setup_errors), from the line runner_hint matched."""
471
+ if self.expired():
472
+ return {"kind": "expired", "fix": "Refresh Mobster's helper", "action": "renew", "raw": EXPIRED_HINT}
473
+ if self.runner_waiting() == "unlock":
474
+ # Nothing to press: it starts by itself once the phone is unlocked.
475
+ return {"kind": "locked", "fix": LOCKED_START_HINT, "action": None, "raw": LOCKED_START_HINT}
476
+ line = getattr(self, "runner_line", None)
477
+ return setup_errors.translate(line) if line else None
478
+
479
+ # -- actions ---------------------------------------------------------------
480
+
481
+ def start_build(self, team=None, renewal=False):
482
+ if team is None:
483
+ # the saved team signs only while Xcode still lists it: a stale team never signs a build nobody picked
484
+ team = self.settings().get("team")
485
+ if not TEAM_PATTERN.fullmatch(team or "") or team not in {known["id"] for known in self.teams()}:
486
+ raise ValueError("Choose your Apple Account")
487
+ if not TEAM_PATTERN.fullmatch(team or ""):
488
+ raise ValueError("A team id is 10 letters and digits")
489
+ device = self.device()
490
+ if device is None:
491
+ raise LookupError("Connect and choose your iPhone first")
492
+ tools = self.tools()
493
+ if tools["xcode"]["state"] != "ok" or not tools["git"]:
494
+ raise LookupError("Finish installing Xcode first (Setup › Get your Mac ready)")
495
+ with self.lock:
496
+ if self.build["state"] == "running":
497
+ return
498
+ # The first build also fetches WebDriverAgent and prepares the phone, so it takes longest.
499
+ self.build = {"state": "running", "error": None, "log_tail": [], "started_at": time.time(),
500
+ "first": not any(self.derived.glob("Build/Products/*.xctestrun")), "renewal": renewal,
501
+ "team": team}
502
+ self.save_settings(udid=device["udid"], device_name=device["name"])
503
+ threading.Thread(target=self._build, args=(team, device["udid"]), name="mobster-wda-build",
504
+ daemon=True).start()
505
+
506
+ def _build(self, team, udid):
507
+ log_path = self.logs / "wda-build.log"
508
+ env_file.private_dir(self.logs)
509
+ tail = []
510
+ if not BUILD_LOCK.acquire(blocking=False):
511
+ with self.lock:
512
+ self.build["log_tail"] = ["Waiting for another iPhone's build to finish."]
513
+ BUILD_LOCK.acquire()
514
+ try:
515
+ self._build_locked(team, udid, log_path, tail)
516
+ finally:
517
+ BUILD_LOCK.release()
518
+
519
+ def _build_locked(self, team, udid, log_path, tail):
520
+ try:
521
+ with env_file.open_private(log_path, "w") as log:
522
+ git = mac_tools.tool("git") or "git"
523
+ if self.project.exists() and not wda_source.is_pinned(git, self.project):
524
+ shutil.rmtree(self.project) # an older or unpinned checkout: fetch the pinned release
525
+ if not self.project.exists():
526
+ self._step(wda_source.clone_command(git, self.project), log, tail)
527
+ wda_source.verify(git, self.project)
528
+ log.write(f"WebDriverAgent {wda_source.REF} ({wda_source.COMMIT})\n")
529
+ self._rebrand(team)
530
+ self._drop_stale_profiles(team, log)
531
+ try:
532
+ wda_patches.patch_wda(self.project)
533
+ except RuntimeError as error:
534
+ tail.append(f"error: {error}") # shown as the build's error
535
+ log.write(tail[-1] + "\n")
536
+ raise
537
+ # USE_IP goes into the built test plan: WDA then listens on the phone's loopback,
538
+ # which USB (usbmux) reaches, and not on its Wi-Fi address.
539
+ self._step([mac_tools.tool("xcodebuild"), "-project", str(self.project / "WebDriverAgent.xcodeproj"),
540
+ "-scheme", "WebDriverAgentRunner", "-destination", f"id={udid}",
541
+ "-allowProvisioningUpdates", "-derivedDataPath", str(self.derived),
542
+ f"DEVELOPMENT_TEAM={team}", f"USE_IP={LOOPBACK}", "build-for-testing"], log, tail)
543
+ self._remove_legacy_runner(team, udid, log)
544
+ # autostart: the watch loop starts the new helper (autostart()), whichever window is open or none. Before,
545
+ # only the Mac app's setup watcher started it, and it isn't mounted during the first run: after the first
546
+ # install the helper stayed stopped with "Starting Mobster's helper" on screen (17 Oct).
547
+ self.save_settings(team=team, built_for=udid, built_patches=wda_patches.patch_digest(),
548
+ expires_at=signing.profile_expiry(self.derived), autostart=True)
549
+ with self.lock:
550
+ self.build = {**self.build, "state": "succeeded", "error": None, "problem": None,
551
+ "log_tail": tail[-LOG_TAIL:]}
552
+ except Exception as error:
553
+ text = "\n".join(tail) if tail else str(error)
554
+ with self.lock:
555
+ self.build = {**self.build, "state": "failed", "error": build_hint(tail) if tail else str(error)[:300],
556
+ "problem": self._team_problem(setup_errors.problem(text), team),
557
+ "log_tail": tail[-LOG_TAIL:]}
558
+
559
+ def _team_problem(self, problem, team):
560
+ if not problem or problem.get("kind") != "xcode_signin" \
561
+ or not re.search(r"no account for team", problem.get("raw") or "", re.I):
562
+ return problem
563
+ try:
564
+ known = {item["id"] for item in self.teams()}
565
+ except Exception: # noqa: BLE001 -- unreadable: keep the sign-in advice
566
+ return problem
567
+ if not known or team in known:
568
+ return problem
569
+ return {**problem, "kind": "team_account", "action": "choose_team",
570
+ "fix": f"Xcode has no Apple Account for team {team}. Choose another team"}
571
+
572
+ def _step(self, command, log, tail):
573
+ # git and xcodebuild come from Xcode, never the /usr/bin stubs, and run with its developer directory.
574
+ if command[0] in XCODE_SHIMS:
575
+ command = [mac_tools.tool(command[0]) or command[0], *command[1:]]
576
+ name = Path(command[0]).name
577
+ if self.watching.is_set():
578
+ raise RuntimeError(f"Mobster is closing: {name} did not start")
579
+ process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
580
+ stdin=subprocess.DEVNULL, start_new_session=True, env=self.xcode_env())
581
+ BUILD_STEPS.add(process)
582
+ self.build_step = process
583
+ started = time.monotonic()
584
+ timer = threading.Timer(supervised.BUILD_STEP_LIMIT, stop_group, (process,))
585
+ timer.daemon = True
586
+ timer.start()
587
+ try:
588
+ for line in process.stdout:
589
+ log.write(line)
590
+ tail.append(line.rstrip()[:400])
591
+ del tail[:-LOG_TAIL * 5]
592
+ with self.lock:
593
+ self.build["log_tail"] = tail[-LOG_TAIL:]
594
+ code = process.wait()
595
+ finally:
596
+ timer.cancel()
597
+ process.stdout.close()
598
+ BUILD_STEPS.discard(process)
599
+ self.build_step = None
600
+ if code != 0 and time.monotonic() - started >= supervised.BUILD_STEP_LIMIT:
601
+ raise RuntimeError(f"{name} ran for {supervised.BUILD_STEP_LIMIT // 60} minutes and was stopped")
602
+ if code != 0:
603
+ raise RuntimeError(f"{name} exited with {code}")
604
+
605
+ def _rebrand(self, team=None):
606
+ """Mobster's bundle ids in the clone; the runner's carries the team's suffix (see BUNDLE_REBRAND)."""
607
+ project = self.project / "WebDriverAgent.xcodeproj" / "project.pbxproj"
608
+ text = project.read_text()
609
+ for old, new in BUNDLE_REBRAND.items():
610
+ if old != "com.facebook.WebDriverAgentRunner":
611
+ text = text.replace(old, new)
612
+ text = RUNNER_ID.sub(signing.runner_bundle_id(team), text)
613
+ project.write_text(text)
614
+
615
+ def _remove_legacy_runner(self, team, udid, log):
616
+ # the plain-id runner earlier versions installed is a second icon and takes one of a free Apple ID's 3 app slots
617
+ if signing.runner_bundle_id(team) == signing.RUNNER_BUNDLE or udid in self.settings().get("legacy_removed", []):
618
+ return
619
+ xcrun = mac_tools.tool("xcrun")
620
+ if not xcrun:
621
+ return
622
+ try:
623
+ result = subprocess.run([xcrun, "devicectl", "device", "uninstall", "app", "--device", udid,
624
+ LEGACY_RUNNER], capture_output=True, text=True, timeout=90, env=self.xcode_env())
625
+ except (OSError, subprocess.SubprocessError) as error:
626
+ log.write(f"Could not remove the old runner ({LEGACY_RUNNER}): {error}\n")
627
+ return
628
+ output = (result.stdout + result.stderr).strip()
629
+ if result.returncode == 0:
630
+ log.write(f"Removed the old runner ({LEGACY_RUNNER}) from the iPhone.\n")
631
+ elif re.search(r"not (installed|found)|no such app|couldn.t find|does not exist", output, re.I):
632
+ log.write(f"The old runner ({LEGACY_RUNNER}) isn't on the iPhone.\n")
633
+ else:
634
+ log.write(f"Could not remove the old runner ({LEGACY_RUNNER}): {output[-300:]}\n")
635
+ return
636
+ self.save_settings(legacy_removed=[*self.settings().get("legacy_removed", []), udid][-8:])
637
+
638
+ def _drop_stale_profiles(self, team, log):
639
+ # otherwise -allowProvisioningUpdates signs with the cached, expiring profile again
640
+ window = signing.renew_window()
641
+ expires = self.signature_expiry()
642
+ if expires is None or expires - time.time() > window:
643
+ return
644
+ for path in signing.stale_runner_profiles(team, time.time() + window):
645
+ try:
646
+ path.unlink()
647
+ log.write(f"Removed an expiring runner profile: {path.name}\n")
648
+ except OSError:
649
+ pass
650
+
651
+ def start_runner(self):
652
+ with self.runner_lock:
653
+ if self.transport == "wifi" and self.wifi_address and self.attached() is not True:
654
+ # A runner on Wi-Fi restarts on Wi-Fi (Setup's Start, a wedged runner), while the phone stays unplugged.
655
+ self._start_wifi_runner(self.wifi_address)
656
+ return
657
+ self._start_runner()
658
+
659
+ def _start_runner(self):
660
+ # Every start (Setup's Start, autostart, probe()'s restart, a renewal's restart) runs here under
661
+ # runner_lock, and close() sets ``watching`` before it takes that lock to stop the runner: a start
662
+ # that gets here afterwards would undo that stop.
663
+ if self.watching.is_set():
664
+ raise LookupError("Mobster is quitting")
665
+ settings = self.settings()
666
+ device = self.device()
667
+ if device is None:
668
+ raise LookupError("Connect your iPhone first")
669
+ if self.expired():
670
+ raise LookupError(EXPIRED_HINT)
671
+ if not self.built() or not settings.get("team"):
672
+ raise LookupError("Build the runner first")
673
+ udid = device["udid"]
674
+ relay = mac_tools.relay_command(udid, self.wda_port, self.video_port)
675
+ self.stop_runner()
676
+ # Our own relay is stopped: a listener still on these ports is another Mobster's (DR-68). Never take it over,
677
+ # and never drive whatever phone answers there.
678
+ from .devices import listening
679
+ if listening(self.wda_port) or listening(self.video_port):
680
+ raise LookupError(f"Another Mobster is using port {self.wda_port} or {self.video_port} on this Mac, "
681
+ "so this iPhone's helper didn't start. Quit the other Mobster, then try again.")
682
+ wda_patches.pin_loopback(self.derived / "Build" / "Products")
683
+ try:
684
+ self.runner_log_start = (self.logs / "wda-runner.log").stat().st_size
685
+ except OSError:
686
+ self.runner_log_start = 0
687
+ # A new runner's health is unknown. ``watching`` and ``runner_lock`` stay the ones __init__ made: the
688
+ # watch loop waits on that Event (close() sets it) and a second start waits on that lock.
689
+ self.health = {"responsive": None, "locked": None, "since": None, "probed_at": 0.0, "ready": None}
690
+ if self.driver() == "xcodebuild":
691
+ command, env = [
692
+ mac_tools.tool("xcodebuild"), "-project", str(self.project / "WebDriverAgent.xcodeproj"),
693
+ "-scheme", "WebDriverAgentRunner", "-destination", f"id={udid}",
694
+ "-derivedDataPath", str(self.derived), f"DEVELOPMENT_TEAM={settings['team']}",
695
+ f"USE_IP={LOOPBACK}", "test-without-building"], self.runner_env()
696
+ else:
697
+ command, env = self._signed_runner_command(udid, settings)
698
+ self.runner = supervised.Supervised("wda-runner", command, self.logs / "wda-runner.log", env=env,
699
+ gate=self._relaunch_gate, hold=self._runner_hold)
700
+ self.relay = supervised.Supervised("usb-relay", relay, self.logs / "usb-relay.log", gate=self._relaunch_gate)
701
+ self.runner_started_at = time.monotonic()
702
+ self.relay.start()
703
+ self.runner.start()
704
+ self.transport = "usb"
705
+ self.save_settings(autostart=True)
706
+
707
+ def _signed_runner_command(self, udid, settings):
708
+ """The launch of a runner signed on this Mac (team_signing), with the environment it runs in: no Xcode's."""
709
+ from .team_signing import tooling
710
+ bundle_id = (settings.get("signed_runner") or {}).get("bundle_id")
711
+ if not bundle_id:
712
+ raise LookupError("Set the helper up again (Setup › Install Mobster's helper)")
713
+ env = tooling.runner_env("phone")
714
+ if self.driver() == "go-ios":
715
+ program = tooling.go_ios()
716
+ command = tooling.go_ios_launch_command(program, udid, bundle_id, env) if program else None
717
+ else:
718
+ program = tooling.pmd3()
719
+ command = tooling.launch_command(program, udid, bundle_id, env) if program else None
720
+ if command is None:
721
+ raise LookupError(f"{self.driver()} isn't on this Mac, so Mobster's helper can't start")
722
+ return command, {**os.environ, "NO_COLOR": "1", "TERM": "dumb", "COLUMNS": "200"}
723
+
724
+ def start_wifi_runner(self, address, peer_udid):
725
+ with self.runner_lock:
726
+ if str(peer_udid or "").upper() != str(self.pinned or self.settings().get("udid") or "").upper():
727
+ raise LookupError("CoreDevice reported a different iPhone for that Wi-Fi address, so Mobster didn't "
728
+ "start on it")
729
+ self._start_wifi_runner(address)
730
+
731
+ def wifi_udid(self):
732
+ """This device's UDID while its runner runs over Wi-Fi, else None."""
733
+ running = self.runner is not None and self.runner.running
734
+ return (self.pinned or self.settings().get("udid")) if running and self.transport == "wifi" else None
735
+
736
+ def _start_wifi_runner(self, address):
737
+ from .wireless import relay as wifi_relay
738
+ from .wireless import xctestrun
739
+ if self.watching.is_set():
740
+ raise LookupError("Mobster is quitting")
741
+ settings = self.settings()
742
+ udid = self.pinned or settings.get("udid")
743
+ if not udid:
744
+ raise LookupError("Choose your iPhone in Setup first")
745
+ if self.expired():
746
+ raise LookupError(EXPIRED_HINT)
747
+ if not self.built() or not settings.get("team"):
748
+ raise LookupError("Build the runner first")
749
+ xcodebuild = mac_tools.tool("xcodebuild")
750
+ if not xcodebuild:
751
+ raise LookupError("Finish installing Xcode first (Setup › Get your Mac ready)")
752
+ try:
753
+ # A tunnel address (fd00::/8) that this Mac reaches through the CoreDevice tunnel, never through Wi-Fi.
754
+ target = wifi_relay.require_tunnel(address)
755
+ plan = xctestrun.write(self.derived / "Build" / "Products", self.derived / xctestrun.FOLDER, target)
756
+ except ValueError as error:
757
+ raise LookupError(str(error)) from None
758
+ self._stop_runner(False)
759
+ relay = wifi_relay.Relay(target, [(self.wda_port, WDA_PORT), (self.video_port, VIDEO_PORT)])
760
+ try:
761
+ relay.start()
762
+ except OSError:
763
+ raise LookupError(f"Another program is using port {self.wda_port} or "
764
+ f"{self.video_port} on this Mac") from None
765
+ try:
766
+ self.runner_log_start = (self.logs / "wda-runner.log").stat().st_size
767
+ except OSError:
768
+ self.runner_log_start = 0
769
+ self.health = {"responsive": None, "locked": None, "since": None, "probed_at": 0.0, "ready": None}
770
+ self.relay = relay
771
+ self.runner = supervised.Supervised("wda-runner", wifi_runner_command(xcodebuild, plan, udid),
772
+ self.logs / "wda-runner.log", env=self.runner_env(),
773
+ gate=self._relaunch_gate, hold=self._runner_hold)
774
+ self.runner_started_at = time.monotonic()
775
+ self.runner.start()
776
+ self.transport, self.wifi_address = "wifi", target
777
+
778
+ def stop_runner(self, remember=False):
779
+ with self.runner_lock:
780
+ self._stop_runner(remember)
781
+
782
+ def check_exposure(self, hostnames=None):
783
+ with self.runner_lock:
784
+ runner = self.runner
785
+ if runner is None or not runner.running or self.exposure_checked == self.runner_started_at:
786
+ return
787
+ if not self.wda_ready():
788
+ return
789
+ self.exposure_checked = self.runner_started_at
790
+ from .wireless import devicectl, exposure
791
+ udid = self.pinned or self.settings().get("udid")
792
+ if hostnames is None:
793
+ try:
794
+ peer = devicectl.list_devices(timeout=4.0).get(str(udid).upper())
795
+ except (devicectl.DevicectlError, RuntimeError):
796
+ peer = None
797
+ hostnames = peer.hostnames if peer is not None else ()
798
+ _addresses, hits = exposure.exposed(hostnames)
799
+ with self.runner_lock:
800
+ if self.runner is not runner:
801
+ return # restarted while looking: the new start is checked on its own
802
+ self.exposure = hits or None
803
+ if hits: # stays stopped until the person starts it again, which checks it again
804
+ self._stop_runner(remember=True)
805
+
806
+ def _stop_runner(self, remember):
807
+ services = [service for service in (self.runner, self.relay) if service is not None]
808
+ for service in services:
809
+ signal_stop = getattr(service, "signal_stop", None)
810
+ if callable(signal_stop):
811
+ signal_stop() # both at once: the second never waits out the first's grace
812
+ for service in services:
813
+ service.stop()
814
+ self.runner = self.relay = None
815
+ self.transport = None
816
+ if remember:
817
+ self.save_settings(autostart=False)
818
+
819
+ def autostart(self):
820
+ """Resume the runner if it was running before and can run now."""
821
+ try:
822
+ with self.runner_lock:
823
+ if (self.settings().get("autostart") and (self.runner is None or not self.runner.running)
824
+ and self.built() and self.device() and not self.wda_alive()):
825
+ first = self.runner is None and self.watchdog.last_restart_at is None
826
+ if not first and self.watchdog.admit(time.monotonic()) is not None:
827
+ return # a relaunch goes through the same admission and backoff as the supervisor's
828
+ try:
829
+ self._start_runner()
830
+ finally:
831
+ if not first:
832
+ self.watchdog.done(True)
833
+ except Exception:
834
+ pass
835
+
836
+ def rearm_on_attach(self):
837
+ """The chosen phone was off the cable and is plugged in now: start the helper again at once (DR-77).
838
+
839
+ A helper started while the phone was away is an ``xcodebuild`` that waits for its destination, and it looks
840
+ for the phone only every half hour or so, so it never sees the cable go in. This sees the edge (the chosen
841
+ phone's UDID, not any phone, listed after it was not) and starts the runner through the same admission and
842
+ backoff as ``autostart``. It starts nothing when a helper already answers, when the phone was not seen away
843
+ first (a launch is ``autostart``'s), or when what was plugged in is another phone. The helper is the chosen
844
+ phone's by UDID, and every task is checked against the phone it names again before it acts
845
+ (wda.identity.verify)."""
846
+ settings = self.settings()
847
+ udid = self.pinned or settings.get("udid")
848
+ if not udid or not settings.get("autostart"):
849
+ self._attached_before = None
850
+ return False
851
+ now_attached = self.attached(udid)
852
+ before, self._attached_before = self._attached_before, now_attached
853
+ if before is not False or now_attached is not True:
854
+ return False
855
+ try:
856
+ with self.runner_lock:
857
+ if not (self.built() and not self.expired() and self.device() and not self.wda_ready(timeout=1.5)):
858
+ return False
859
+ if self.watchdog.admit(time.monotonic()) is not None:
860
+ return False
861
+ try:
862
+ self._start_runner()
863
+ finally:
864
+ self.watchdog.done(True)
865
+ return True
866
+ except Exception: # noqa: BLE001 -- the next attach (or autostart) tries again
867
+ return False
868
+
869
+ def probe(self, now=None):
870
+ """Check that WDA can reach the phone's UI; restart a runner wedged for too long."""
871
+ now = time.monotonic() if now is None else now
872
+ if now - self.health["probed_at"] < PROBE_INTERVAL:
873
+ return
874
+ self.health["probed_at"] = now
875
+ ready = self.wda_ready()
876
+ self.health["ready"] = ready
877
+ if not ready:
878
+ self.health.update(responsive=None, locked=None, since=None)
879
+ # A runner Mobster started, whose process is up and past its start-up grace, that doesn't answer /status
880
+ # is wedged. A runner stopped on purpose, still starting or not ours is not. Its start-up grace counts from
881
+ # its newest launch: one held back while the phone was locked launches long after start_runner.
882
+ started = self.runner_started_at
883
+ launched = getattr(self.runner, "launched_at", None)
884
+ if started is not None and isinstance(launched, (int, float)):
885
+ started = max(started, launched)
886
+ if (self.runner is not None and self._runner_alive() and started is not None
887
+ and time.monotonic() - started >= STARTUP_GRACE_SECONDS):
888
+ self.watchdog.probe_result(False, now)
889
+ else:
890
+ self.watchdog.reset_failures()
891
+ return
892
+ try:
893
+ locked = self._wda_get("/wda/locked", PROBE_TIMEOUT).get("value")
894
+ self.health.update(responsive=True, locked=locked is True, since=None)
895
+ self.watchdog.probe_result(True, now)
896
+ return
897
+ except Exception:
898
+ since = self.health["since"] or now
899
+ self.health.update(responsive=False, locked=None, since=since)
900
+ if self.watchdog.probe_result(False, now):
901
+ self.health.update(since=None)
902
+
903
+ def _restart_wedged_runner(self):
904
+ self.start_runner() # stops the wedged runner and relay first
905
+
906
+ def poll_presence(self):
907
+ """One lazy, read-only poll of whether the owner is using the phone, and the idle release."""
908
+ if not self.wda_ready():
909
+ self.presence.observe(ok=False)
910
+ return
911
+ self.presence_poller.poll()
912
+ if self.presence.should_release():
913
+ for hook in list(self.idle_hooks):
914
+ try:
915
+ hook()
916
+ except Exception: # noqa: BLE001
917
+ pass
918
+
919
+ def watch(self, stop):
920
+ next_round = 0.0
921
+ while not stop.is_set():
922
+ try:
923
+ self.probe()
924
+ except Exception:
925
+ pass
926
+ if time.monotonic() >= next_round:
927
+ next_round = time.monotonic() + WATCH_INTERVAL
928
+ self.watch_round()
929
+ stop.wait(PROBE_TICK)
930
+
931
+ def watch_round(self):
932
+ """One pass of the watch loop's slower jobs. Each one's failure is its own."""
933
+ for job in (self.autostart, self.rearm_on_attach, self.poll_presence, self.check_exposure,
934
+ self.retry_locked_build):
935
+ try:
936
+ job()
937
+ except Exception:
938
+ pass
939
+ if self.renewal is not None:
940
+ try:
941
+ self.renewal.tick()
942
+ except Exception:
943
+ pass
944
+
945
+ def engine_reading(self):
946
+ from .wireless import transport
947
+ settings = self.settings()
948
+ udid = self.pinned or settings.get("udid")
949
+ state = self.runner_state(ready=self.health["ready"])
950
+ wifi = self.transport == "wifi"
951
+ return {"chosen": bool(udid), "built": self.built(), "expired": self.expired(),
952
+ "exposed": bool(self.exposure), "runner": state["state"], "holding": self.runner_waiting(),
953
+ "hint": self.runner_cause if state["state"] != "running" else None,
954
+ "locked": self.health["locked"], "responsive": self.health["responsive"],
955
+ "usb": None if wifi else self.attached(), "wifi": wifi,
956
+ # The link layer checks every 10 s (wireless.monitor.TICK); a Wi-Fi helper the probe finds silent is
957
+ # named at the probe's pace instead, so the cause is said inside engine_health.NAMED_WITHIN_S.
958
+ "wifi_lost": bool(wifi and (self.health["ready"] is False or udid and transport.lost(udid)))}
959
+
960
+ def begin_close(self):
961
+ self.watching.set()
962
+ for service in (self.runner, self.relay):
963
+ signal_stop = getattr(service, "signal_stop", None)
964
+ if callable(signal_stop):
965
+ signal_stop()
966
+ step = self.build_step
967
+ if step is not None:
968
+ try:
969
+ os.killpg(step.pid, signal.SIGTERM)
970
+ except OSError:
971
+ pass
972
+
973
+ def close(self):
974
+ self.watching.set()
975
+ step = self.build_step
976
+ if step is not None:
977
+ stop_group(step)
978
+ self.stop_runner()