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,952 @@
1
+ """The simulator manager (SPEC §12.1): headless simulators Mobster creates, boots and keeps warm."""
2
+
3
+ import datetime
4
+ import os
5
+ import threading
6
+ import time
7
+ from pathlib import Path
8
+ from typing import Callable, Optional
9
+ from urllib.parse import urlsplit
10
+
11
+ from ..devices import LOOPBACK, wda_url
12
+ from . import apps, holders, simctl, xcode
13
+ from .governor import MAX_SIMS_CEILING, SIM_GB, Governor
14
+ from .models import SIM_KINDS, AppInfo, SimError, SimLease, SimTarget # noqa: F401 (SIM_KINDS: api is the contract)
15
+ from .registry import Registry, free_slot, now, port_base, taken_ports
16
+ from .wda import MJPEG_WAIT, MOVE, RESTART, REUSE, SimulatorWDA, WebDriverAgent, build_dir, find_xctestrun, heartbeat
17
+
18
+ # The settings _configure writes after a boot, and again whenever the runner (re)starts.
19
+ KEYBOARD_DEFAULTS = ("KeyboardAutocorrection", "KeyboardPrediction", "SmartQuotesEnabled", "SmartDashesEnabled")
20
+ KEYBOARD_PREFERENCES = "com.apple.keyboard.preferences"
21
+ # The slide-typing introduction's Continue button is in no app's tree, so it is marked as already shown.
22
+ KEYBOARD_INTRO = (("DidShowContinuousPathIntroduction", "true"), ("DidShowGestureKeyboardIntroduction", "true"),
23
+ ("KeyboardDidShowProductivityTutorial", "true"), ("KeyboardContinuousPathEnabled", "false"))
24
+ STATUS_BAR = ("--time", "9:41", "--dataNetwork", "wifi", "--wifiBars", "3", "--cellularBars", "4",
25
+ "--batteryState", "charged", "--batteryLevel", "100")
26
+ BOOT_TIMEOUT = 240
27
+ OPEN_PROMPT = "Open in" # iOS 26's "Open in “<App>”?" after `simctl openurl` with a custom scheme
28
+ OPEN_BUTTON = "Open"
29
+ # iOS remembers an Open in com.apple.launchservices.schemeapproval as "<opener>--><scheme>" = <bundle id>, and
30
+ # `simctl openurl` opens links as CoreSimulatorBridge. Writing that answer when Mobster installs an app means
31
+ # its own links never stop at the prompt (measured on iOS 26.4, 28 Sep).
32
+ SCHEME_APPROVALS = "com.apple.launchservices.schemeapproval"
33
+ URL_OPENER = "com.apple.CoreSimulator.CoreSimulatorBridge"
34
+ OPEN_PROMPT_WAIT = 3.0 # how long open_url looks for that prompt, in reads the runner answered
35
+ OPEN_PROMPT_LIMIT = 30.0 # and at most, when the runner is slow to answer
36
+ ALERT_GONE_WAIT = 2.0 # how long an answered alert has to leave the screen
37
+ ALERT_POLL = 0.2
38
+ SWEEP_ROUNDS = 3 # alerts dismissed in a row before a simulator is handed out
39
+ DEFAULT_MAX_SIMS = MAX_SIMS_CEILING
40
+ PRUNE_DAYS = 14
41
+ BUSY_POLL = 1.0
42
+ PROBE_RETRY = 0.2 # before creating another simulator: a list() probe holds a lease for milliseconds
43
+ XCRUN = simctl.XCRUN
44
+
45
+
46
+ def _device_lease(wda_url):
47
+ from ..journal import Lease
48
+ return Lease.device(wda_url)
49
+
50
+
51
+ def _port(url):
52
+ return urlsplit(url).port
53
+
54
+
55
+ def _parse_time(text):
56
+ try:
57
+ return datetime.datetime.strptime(str(text), "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=datetime.timezone.utc)
58
+ except (TypeError, ValueError):
59
+ return None
60
+
61
+
62
+ class SimulatorManager:
63
+ """Creates, boots and keeps Mobster's simulators, and runs the app under test on them."""
64
+
65
+ # Mobster's own simulators, never someone's phone: verify.smart.is_real_device treats a manager that doesn't
66
+ # say as a phone.
67
+ real_device = False
68
+
69
+ def __init__(self, data_dir: Optional[Path] = None, progress: Optional[Callable[[str], None]] = None):
70
+ from ..paths import dev_data_dir
71
+ self.data_dir = Path(data_dir).expanduser() if data_dir else dev_data_dir()
72
+ # also called from the thread that builds WebDriverAgent while a simulator boots: it must be thread-safe
73
+ self.progress = progress or (lambda message: None)
74
+ self.registry = Registry(self.data_dir)
75
+ self.run = simctl.run
76
+ self.wda = WebDriverAgent(self.data_dir, log=self._log, progress=self._say)
77
+ self.lease = _device_lease
78
+ self.sleep = time.sleep
79
+ self.clock = time.monotonic
80
+ self._tools = None
81
+ self._approved = {} # udid -> {scheme}: links this manager knows open without the prompt
82
+ self.governor = Governor()
83
+ # who is asking, for the holder record `mobster sim list` shows: {"client": ..., "session": ...}
84
+ self.who = lambda: {"client": "mobster", "session": f"pid {os.getpid()}"}
85
+
86
+ # -- plumbing ------------------------------------------------------------------------------------------
87
+
88
+ def _say(self, message):
89
+ self._log(message)
90
+ try:
91
+ self.progress(message)
92
+ except Exception: # a progress sink must never break a run
93
+ pass
94
+
95
+ def _log(self, message):
96
+ try:
97
+ folder = self.data_dir / "logs"
98
+ folder.mkdir(parents=True, exist_ok=True)
99
+ with open(folder / "sim.log", "a", encoding="utf-8") as stream:
100
+ stream.write(f"{now()} {message}\n")
101
+ except OSError:
102
+ pass
103
+
104
+ @property
105
+ def max_sims(self):
106
+ text = str(os.environ.get("MOBSTER_MAX_SIMS") or "").strip()
107
+ if not text:
108
+ return DEFAULT_MAX_SIMS
109
+ try:
110
+ value = int(text)
111
+ except ValueError:
112
+ value = 0
113
+ if value < 1:
114
+ raise SimError("environment", f"MOBSTER_MAX_SIMS is {text!r}; it must be a whole number of 1 or more.",
115
+ f"Unset it to allow {DEFAULT_MAX_SIMS} simulators per device type and runtime.")
116
+ return value
117
+
118
+ def tools(self):
119
+ """Xcode's tools, read once per manager. SimError("environment") without a usable Xcode."""
120
+ if self._tools is None:
121
+ self._tools = xcode.find_tools(self.run)
122
+ return self._tools
123
+
124
+ def simctl(self, *args, timeout=60, env=None):
125
+ result = self.run([XCRUN, "simctl", *map(str, args)], timeout=timeout, env=env or self.tools().env)
126
+ if not result.ok:
127
+ self._log(f"simctl {' '.join(map(str, args[:3]))}: exit {result.code}: {result.last_line()}")
128
+ return result
129
+
130
+ def _json(self, *args):
131
+ import json
132
+ result = self.simctl("list", *args, "-j", timeout=60)
133
+ try:
134
+ if not result.ok:
135
+ raise ValueError
136
+ return json.loads(result.out)
137
+ except ValueError:
138
+ raise SimError("environment", f"simctl couldn't list simulators: {result.last_line() or 'no output'}",
139
+ "Open Xcode once so it finishes installing its components, then run "
140
+ "`mobster sim doctor`.") from None
141
+
142
+ def _catalog(self):
143
+ data = self._json()
144
+ return simctl.parse_runtimes(data), simctl.parse_device_types(data), simctl.parse_devices(data)
145
+
146
+ def _devices(self):
147
+ return simctl.parse_devices(self._json("devices"))
148
+
149
+ def _choose(self, device=None, runtime=None, catalog=None):
150
+ runtimes, types, devices = catalog or self._catalog()
151
+ chosen_runtime = simctl.choose_runtime(runtimes, runtime)
152
+ return simctl.choose_device_type(types, chosen_runtime, device), chosen_runtime, devices
153
+
154
+ def _try_lease(self, wda_url):
155
+ """The device lease for this WDA address, or None while another process holds it."""
156
+ from ..journal import JournalError, LeaseHeld
157
+ try:
158
+ return self.lease(wda_url)
159
+ except LeaseHeld:
160
+ return None
161
+ except JournalError as exc:
162
+ raise SimError("simulator", f"Mobster couldn't take the simulator's lock: {exc}",
163
+ "Check that ~/Library/Application Support/app.mobster.desktop/device-leases belongs "
164
+ "to you and is private.") from None
165
+
166
+ # -- acquire -------------------------------------------------------------------------------------------
167
+
168
+ def acquire(self, device: Optional[str] = None, runtime: Optional[str] = None, *,
169
+ timeout: float = 600, cancelled=None) -> SimLease:
170
+ def claim(timing):
171
+ device_type, chosen_runtime, entry, state, lease = self._claim(device, runtime, timing)
172
+ if lease is None:
173
+ from ..verify.actions import ActionNotDispatched
174
+ busy = ActionNotDispatched(self._all_busy(device_type, chosen_runtime))
175
+ else:
176
+ busy = None
177
+ return entry, state, lease, busy, f"Waiting for a free simulator: all {self.max_sims} are in use"
178
+ return self._acquire(claim, timeout, "", cancelled=cancelled, claim_may_create=True)
179
+
180
+ def acquire_udid(self, udid: str, *, timeout: float = 600, cancelled=None) -> SimLease:
181
+ def claim(timing):
182
+ with self.registry.edit() as entries:
183
+ entry = self._entry(udid, entries)
184
+ present = {item.udid: item for item in self._devices()}
185
+ if entry["udid"] not in present:
186
+ raise SimError("simulator", f"{entry['name']} no longer exists.", "Run `mobster sim prune`.")
187
+ lease = self._try_lease(wda_url(entry["wda_port"]))
188
+ if lease is not None:
189
+ entry["last_used_at"] = now()
190
+ return dict(entry), present[entry["udid"]].state, lease, None, ""
191
+ name = entry["name"]
192
+ # No lease was acquired and _make_ready has not run; this timeout cannot hide a device effect.
193
+ from ..verify.actions import ActionNotDispatched
194
+ busy = ActionNotDispatched(SimError(
195
+ "busy", f"{name} is running a check.", "Wait for it to finish, or stop it, then try again."))
196
+ return None, None, None, busy, f"Waiting for {name}: it is running a check"
197
+ return self._acquire(claim, timeout, " by UDID", cancelled=cancelled)
198
+
199
+ def _acquire(self, claim, timeout, how, *, cancelled=None, claim_may_create=False):
200
+ from ..verify.actions import ActionNotDispatched
201
+
202
+ def stopped(*, known):
203
+ error = SimError("stopped", "The suite was stopped before this simulator ran.")
204
+ raise ActionNotDispatched(error) if known else error
205
+
206
+ started = self.clock()
207
+ timing = {}
208
+ if cancelled is not None and cancelled():
209
+ stopped(known=True)
210
+ tools = self.tools()
211
+ deadline, waiting = started + max(0.0, float(timeout)), False
212
+ while True:
213
+ if cancelled is not None and cancelled():
214
+ stopped(known=True)
215
+ entry, state, lease, busy, message = claim(timing)
216
+ if lease is not None:
217
+ break
218
+ if self.clock() >= deadline:
219
+ raise busy
220
+ if not waiting:
221
+ self._say(message)
222
+ waiting = True
223
+ self.sleep(BUSY_POLL)
224
+ held = [lease]
225
+ holders.claim(self.data_dir, entry["udid"], self.who())
226
+ try:
227
+ if cancelled is not None and cancelled():
228
+ stopped(known=not claim_may_create)
229
+ target = self._make_ready(entry, state, held, tools, timing)
230
+ self._sweep_alerts(target)
231
+ except BaseException:
232
+ for item in held:
233
+ item.close()
234
+ holders.clear(self.data_dir, entry["udid"])
235
+ raise
236
+ timing["total"] = round(self.clock() - started, 2)
237
+ self._log(f"acquired {target.name} ({target.udid}) on {target.wda_url}{how}: {timing}")
238
+ return SimLease(target, held, timing, on_release=lambda: holders.clear(self.data_dir, target.udid))
239
+
240
+ def _claim(self, device, runtime, timing):
241
+ with self.registry.edit() as entries:
242
+ # simctl's list is read under the lock: one read before it can miss a simulator another process registered
243
+ device_type, runtime, devices = self._choose(device, runtime)
244
+ present = {item.udid: item for item in devices}
245
+ for entry in [entry for entry in entries if entry["udid"] not in present]:
246
+ entries.remove(entry)
247
+ self._log(f"forgot {entry['udid']} ({entry['name']}): simctl no longer lists it")
248
+ pair = [entry for entry in entries
249
+ if entry["device_type"] == device_type.name and entry["runtime"] == runtime.name]
250
+ pair.sort(key=lambda entry: entry.get("last_used_at") or "", reverse=True)
251
+ pair.sort(key=lambda entry: present[entry["udid"]].state != "Booted")
252
+ entry, lease = self._first_free(pair)
253
+ if lease is None and pair and len(pair) < self.max_sims:
254
+ # Every lease was held, and a create (a cold boot, and 1.6 GB of disk) is next. `list()` and
255
+ # doctor take and release each lease for milliseconds to see whether it is in use, while a run
256
+ # holds its lease for the whole check. So look once more first (review round 3).
257
+ self.sleep(PROBE_RETRY)
258
+ entry, lease = self._first_free(pair)
259
+ if lease is not None:
260
+ if present[entry["udid"]].state != "Booted":
261
+ self._memory_room(lease, entries, present, device_type, runtime, mine=entry)
262
+ entry["last_used_at"] = now()
263
+ return device_type, runtime, dict(entry), present[entry["udid"]].state, lease
264
+ if len(pair) >= self.max_sims:
265
+ return device_type, runtime, None, None, None
266
+ self._memory_room(None, entries, present, device_type, runtime)
267
+ entry, lease = self._create(device_type, runtime, devices, entries, timing)
268
+ return device_type, runtime, dict(entry), "Shutdown", lease
269
+
270
+ def _memory_room(self, lease, entries, present, device_type, runtime, mine=None):
271
+ """Refuse at once, rather than boot one simulator too many: each needs SIM_GB free; the first always boots.
272
+
273
+ A simulator another session has leased but not finished booting counts as running, with its memory set aside:
274
+ until it boots, the free memory doesn't show it, and two sessions booting at once would both see room."""
275
+ booted = [entry for entry in entries if present.get(entry["udid"]) and present[entry["udid"]].state == "Booted"]
276
+ starting = [entry for entry in entries if entry is not mine and entry not in booted
277
+ and present.get(entry["udid"]) and self._in_use(entry)]
278
+ running = len(booted) + len(starting)
279
+ pressure = self.governor.sample()
280
+ if self.governor.memory_room(running, pressure, starting=len(starting)):
281
+ return
282
+ if lease is not None:
283
+ lease.close()
284
+ raise self._all_busy(device_type, runtime, free_gb=pressure.free_gb, running=running)
285
+
286
+ def _all_busy(self, device_type, runtime, free_gb=None, running=None):
287
+ entries = self.registry.read()
288
+ who = [holders.words(record) for entry in entries
289
+ if entry["device_type"] == device_type.name and entry["runtime"] == runtime.name
290
+ for record in [holders.read(self.data_dir, entry["udid"])] if record]
291
+ by = f", held by {'; '.join(who)}" if who else ""
292
+ fix = ("A simulator is free when its session ends or has been idle for 30 minutes. Call again then, or ask "
293
+ "the other agent to stop.")
294
+ if free_gb is not None:
295
+ return SimError("busy", f"Every simulator is busy: only {free_gb:.1f} GB of memory is free and another "
296
+ f"needs {SIM_GB} GB, with {running} in use{by}.", fix)
297
+ return SimError("busy", f"Every simulator is busy: all {self.max_sims} for {device_type.name} with "
298
+ f"{runtime.name} are in use{by}.", fix + " MOBSTER_MAX_SIMS raises the limit.")
299
+
300
+ def _first_free(self, entries):
301
+ """(entry, lease) for the first of ``entries`` whose lease is free, else (None, None)."""
302
+ for entry in entries:
303
+ lease = self._try_lease(wda_url(entry["wda_port"]))
304
+ if lease is not None:
305
+ return entry, lease
306
+ return None, None
307
+
308
+ def _create(self, device_type, runtime, devices, entries, timing):
309
+ name = simctl.sim_name(device_type.name, runtime.name,
310
+ {item.name for item in devices} | {entry["name"] for entry in entries})
311
+ self._say(f"Creating simulator {name}")
312
+ started = self.clock()
313
+ result = self.simctl("create", name, device_type.identifier, runtime.identifier, timeout=120)
314
+ udid = simctl.parse_created(result.out) if result.ok else None
315
+ if not udid:
316
+ raise SimError("simulator", f"simctl couldn't create {name}: {result.last_line() or 'no UDID'}",
317
+ "Run `mobster sim doctor`, then try again.")
318
+ timing["create"] = round(self.clock() - started, 2)
319
+ try:
320
+ wda_port, mjpeg_port, lease = self._claim_ports(taken_ports(entries))
321
+ except BaseException:
322
+ self.simctl("delete", udid, timeout=60) # just created, never registered: nothing else knows it
323
+ raise
324
+ stamp = now()
325
+ entry = {"udid": udid, "name": name, "device_type": device_type.name, "runtime": runtime.name,
326
+ "wda_port": wda_port, "mjpeg_port": mjpeg_port, "created_at": stamp, "last_used_at": stamp}
327
+ entries.append(entry)
328
+ self._log(f"created {name} ({udid}) on ports {wda_port}/{mjpeg_port}")
329
+ return entry, lease
330
+
331
+ def _make_ready(self, entry, state, held, tools, timing):
332
+ udid, name = entry["udid"], entry["name"]
333
+ folder = apps.device_folder(udid)
334
+ if not folder.is_dir():
335
+ raise SimError("simulator", f"{name}'s folder {folder} is missing, so Mobster can't keep its apps "
336
+ "apart from other simulators'.",
337
+ f"Delete it with `mobster sim delete {udid}`, then try again.")
338
+ build = {}
339
+ builder = None
340
+ needs_build = find_xctestrun(build_dir(self.data_dir, tools.build)) is None
341
+ if state != "Booted" or needs_build:
342
+ try:
343
+ self.governor.wait_for_calm(self._say, f"boot {name}" if state != "Booted" else "build WebDriverAgent")
344
+ except SimError as busy:
345
+ # No simctl boot, build or runner start has been issued yet, so a refusal here is a rejected acquire.
346
+ # A simulator this acquire just created is an effect: that refusal stays unknown, as a stop after the
347
+ # claim does (``stopped(known=not claim_may_create)``).
348
+ if "create" in timing:
349
+ raise
350
+ from ..verify.actions import ActionNotDispatched
351
+ raise ActionNotDispatched(busy) from None
352
+ if needs_build:
353
+ def build_runner():
354
+ began = self.clock()
355
+ try:
356
+ build["plan"] = self.wda.ensure_build(tools)
357
+ build["seconds"] = round(self.clock() - began, 2)
358
+ except BaseException as exc: # handed to the waiting thread below
359
+ build["error"] = exc
360
+ # The build (once per Xcode version) runs while the simulator boots.
361
+ builder = threading.Thread(target=build_runner, name="mobster-wda-build", daemon=True)
362
+ builder.start()
363
+ configurer = []
364
+
365
+ def configure_once():
366
+ if configurer:
367
+ return
368
+
369
+ def configure():
370
+ began = self.clock()
371
+ try:
372
+ self._configure(udid)
373
+ except Exception as exc: # the run can go ahead; the log says what was skipped
374
+ self._log(f"settings for {udid} failed: {type(exc).__name__}: {exc}")
375
+ return
376
+ timing["configure"] = round(self.clock() - began, 2)
377
+ configurer.append(threading.Thread(target=configure, name="mobster-sim-configure", daemon=True))
378
+ configurer[0].start()
379
+ try:
380
+ if state != "Booted":
381
+ started = self.clock()
382
+ self._say(f"Booting {name}")
383
+ with heartbeat(self._say, f"booting {name}"):
384
+ self._boot(udid, name, state)
385
+ timing["boot"] = round(self.clock() - started, 2)
386
+ self._say(f"Simulator: {name} (booted in {timing['boot']:.1f} s)")
387
+ configure_once()
388
+ else:
389
+ self._say(f"Simulator: {name}")
390
+ if builder is not None:
391
+ builder.join()
392
+ if "error" in build:
393
+ raise build["error"]
394
+ xctestrun = build["plan"]
395
+ timing["wda_build"] = build["seconds"]
396
+ else:
397
+ xctestrun = self.wda.ensure_build(tools)
398
+ entry = self._runner(entry, held, xctestrun, tools, timing, configure_once)
399
+ except BaseException:
400
+ if builder is not None: # never leave xcodebuild behind a failed acquire (no-op once it finished)
401
+ self.wda.cancel()
402
+ builder.join(timeout=10)
403
+ raise
404
+ finally:
405
+ for thread in configurer:
406
+ thread.join()
407
+ return SimTarget(udid=udid, name=name, device_type=entry["device_type"], runtime=entry["runtime"],
408
+ wda_url=wda_url(entry["wda_port"]), mjpeg_url=wda_url(entry["mjpeg_port"]),
409
+ xctestrun=str(xctestrun))
410
+
411
+ def _boot(self, udid, name, state):
412
+ if state != "Booting":
413
+ result = self.simctl("boot", udid, timeout=BOOT_TIMEOUT)
414
+ text = result.out + result.err
415
+ if not result.ok and "current state: Booted" not in text:
416
+ if result.timed_out:
417
+ raise self._still_booting(name)
418
+ if "Unable to boot" not in text:
419
+ raise self._boot_failed(udid, name, result)
420
+ self.simctl("shutdown", udid, timeout=60)
421
+ self.sleep(1)
422
+ result = self.simctl("boot", udid, timeout=BOOT_TIMEOUT)
423
+ if not result.ok and "current state: Booted" not in result.out + result.err:
424
+ raise self._still_booting(name) if result.timed_out else self._boot_failed(udid, name, result)
425
+ result = self.simctl("bootstatus", udid, "-b", timeout=BOOT_TIMEOUT)
426
+ if not result.ok:
427
+ if result.timed_out:
428
+ raise self._still_booting(name)
429
+ raise SimError("simulator", f"{name} didn't finish booting: {result.last_line() or 'no answer'}",
430
+ f"Erase it with `mobster sim erase {udid}`, then try again.")
431
+
432
+ @staticmethod
433
+ def _still_booting(name):
434
+ return SimError("simulator", f"{name} is still booting after {BOOT_TIMEOUT} s, which happens when this Mac "
435
+ "is busy.", "Run the command again; the simulator keeps booting.")
436
+
437
+ @staticmethod
438
+ def _boot_failed(udid, name, result):
439
+ return SimError("simulator", f"{name} didn't boot: {result.last_line()}",
440
+ f"Erase it with `mobster sim erase {udid}`, then try again.")
441
+
442
+ def _configure(self, udid):
443
+ calls = [("status_bar", udid, "override", *STATUS_BAR)]
444
+ calls += [("spawn", udid, "defaults", "write", "com.apple.Preferences", key, "-bool", "false")
445
+ for key in KEYBOARD_DEFAULTS]
446
+ calls += [("spawn", udid, "defaults", "write", KEYBOARD_PREFERENCES, key, "-bool", value)
447
+ for key, value in KEYBOARD_INTRO]
448
+ self._simctl_all(calls)
449
+
450
+ # each call takes 0.2-0.5 s, mostly waiting on CoreSimulator, so independent ones run at once
451
+ def _simctl_all(self, calls, timeout=30):
452
+ if len(calls) == 1:
453
+ return [self.simctl(*calls[0], timeout=timeout)]
454
+ from concurrent.futures import ThreadPoolExecutor
455
+ with ThreadPoolExecutor(max_workers=len(calls), thread_name_prefix="mobster-simctl") as pool:
456
+ return list(pool.map(lambda call: self.simctl(*call, timeout=timeout), calls))
457
+
458
+ def _runner(self, entry, held, xctestrun, tools, timing, on_start=None):
459
+ udid, name = entry["udid"], entry["name"]
460
+ port, mjpeg = entry["wda_port"], entry["mjpeg_port"]
461
+ plan = self.wda.plan(udid, port, mjpeg, xctestrun)
462
+ self._log(f"runner plan for {udid} on {port}: {plan}")
463
+ if plan == REUSE:
464
+ return entry
465
+ if on_start is not None:
466
+ on_start()
467
+ if plan == MOVE:
468
+ entry = self._move(entry, held)
469
+ self._say(f"Port {port} is in use by another program, so {name}'s WebDriverAgent moves to "
470
+ f"{entry['wda_port']}")
471
+ port, mjpeg = entry["wda_port"], entry["mjpeg_port"]
472
+ self.wda.stop(udid)
473
+ elif plan == RESTART:
474
+ self.wda.stop(udid)
475
+ self._say(f"Starting WebDriverAgent on {LOOPBACK}:{port}")
476
+ started = self.clock()
477
+ process = self.wda.start(udid, port, mjpeg, xctestrun, tools)
478
+ try:
479
+ self.wda.wait_ready(udid, port, process)
480
+ ok, found = self.wda.check_loopback(port, mjpeg)
481
+ missing = [number for number, items in found.items() if not items]
482
+ exposed = any(item.host != LOOPBACK for items in found.values() for item in items)
483
+ if not ok and missing and not exposed:
484
+ raise SimError("wda", f"WebDriverAgent answered on {port} but nothing was seen listening on "
485
+ f"{', '.join(map(str, missing))} within {MJPEG_WAIT:g} s",
486
+ f"Run the command again; see {self.wda.runner_log(udid)}.")
487
+ if not ok:
488
+ seen = "; ".join(f"{number}: " + (", ".join(f"{item.host}:{item.port}" for item in items) or
489
+ "nothing")
490
+ for number, items in found.items())
491
+ raise SimError("wda", "WebDriverAgent listened beyond this Mac",
492
+ f"lsof showed {seen}. Delete {build_dir(self.data_dir, tools.build)} and try again.")
493
+ except BaseException:
494
+ self.wda.stop(udid)
495
+ raise
496
+ timing["wda_start"] = round(self.clock() - started, 2)
497
+ self._say(f"WebDriverAgent ready on {LOOPBACK}:{port} in {timing['wda_start']:.1f} s")
498
+ return entry
499
+
500
+ def _claim_ports(self, taken):
501
+ taken = set(taken)
502
+ for _ in range(40):
503
+ wda_port, mjpeg_port = free_slot(port_base(), taken, busy=self.wda.listening)
504
+ lease = self._try_lease(wda_url(wda_port))
505
+ if lease is not None:
506
+ return wda_port, mjpeg_port, lease
507
+ taken |= {wda_port, mjpeg_port}
508
+ raise SimError("busy", "Every free WebDriverAgent port is locked by another Mobster process.",
509
+ "Wait for the other runs to finish, then try again.")
510
+
511
+ def _move(self, entry, held):
512
+ """Give the entry the next free port pair, holding that address's lease too."""
513
+ with self.registry.edit() as entries:
514
+ wda_port, mjpeg_port, lease = self._claim_ports(
515
+ taken_ports(entries) | {entry["wda_port"], entry["mjpeg_port"]})
516
+ held.append(lease)
517
+ for item in entries:
518
+ if item["udid"] == entry["udid"]:
519
+ item["wda_port"], item["mjpeg_port"] = wda_port, mjpeg_port
520
+ return {**entry, "wda_port": wda_port, "mjpeg_port": mjpeg_port}
521
+
522
+ # -- apps ----------------------------------------------------------------------------------------------
523
+
524
+ def app_info(self, app_path: str) -> AppInfo:
525
+ """The .app's identity from its Info.plist. SimError("install") for a device build or a non-app."""
526
+ info = apps.read_info(app_path)
527
+ bundle = str(info.get("CFBundleIdentifier") or "").strip()
528
+ if not bundle:
529
+ raise SimError("install", f"{app_path} has no CFBundleIdentifier in its Info.plist.",
530
+ "Build the app again.")
531
+ if not apps.built_for_simulator(info):
532
+ raise apps.device_build_error(app_path)
533
+ return AppInfo(bundle_id=bundle, name=apps.display_name(info, bundle), version=apps.version_text(info),
534
+ path=str(Path(app_path).expanduser().resolve()), icon=apps.icon_png(app_path, info),
535
+ executable=_executable(info))
536
+
537
+ def installed_app(self, target: SimTarget, bundle_id: str) -> AppInfo:
538
+ result = self.simctl("get_app_container", target.udid, bundle_id, "app", timeout=30)
539
+ path = result.out.strip() if result.ok else ""
540
+ if not path:
541
+ raise SimError("install", f"{bundle_id} isn't installed on the simulator. Pass --app.",
542
+ "Pass the .app built for the iOS Simulator with --app.")
543
+ info = apps.read_info(path)
544
+ return AppInfo(bundle_id=bundle_id, name=apps.display_name(info, bundle_id),
545
+ version=apps.version_text(info), path=None, icon=apps.icon_png(path, info),
546
+ executable=_executable(info))
547
+
548
+ def install(self, target: SimTarget, app_path: str) -> AppInfo:
549
+ info = self.app_info(app_path)
550
+ # Each scheme as declared and in lowercase, since a link's scheme matches without case.
551
+ schemes = list(dict.fromkeys(form for scheme in apps.url_schemes(apps.read_info(info.path))
552
+ for form in (scheme, scheme.lower())))
553
+ started = self.clock()
554
+ approvals = [("spawn", target.udid, "defaults", "write", SCHEME_APPROVALS, f"{URL_OPENER}-->{scheme}",
555
+ "-string", info.bundle_id) for scheme in schemes]
556
+ result, *approved = self._simctl_all([("install", target.udid, info.path)] + approvals, timeout=300)
557
+ if not result.ok:
558
+ raise SimError("install", f"{info.name} didn't install on {target.name}: "
559
+ f"{result.last_line() or 'simctl failed'}",
560
+ "Build it again for the iOS Simulator, then try again.")
561
+ known = {scheme for scheme, done in zip(schemes, approved) if done.ok and scheme == scheme.lower()}
562
+ self._approved.setdefault(target.udid, set()).update(known)
563
+ self._log(f"installed {info.bundle_id} {info.version} on {target.udid} in {self.clock() - started:.2f} s"
564
+ + (f"; links open without the prompt: {', '.join(sorted(known))}" if known else ""))
565
+ return info
566
+
567
+ def reset(self, target: SimTarget, bundle_id: str, level: str, app_path: Optional[str] = None) -> None:
568
+ if level not in ("none", "data", "reinstall"):
569
+ raise ValueError(f"reset level must be none, data or reinstall, not {level!r}")
570
+ if level != "none" and not bundle_id.startswith("com.apple."):
571
+ started = self.clock()
572
+ if level == "data":
573
+ self._reset_data(target, bundle_id)
574
+ else:
575
+ self._reinstall(target, bundle_id, app_path)
576
+ self._log(f"reset {bundle_id} ({level}) on {target.udid} in {self.clock() - started:.2f} s")
577
+ # After the app is gone, never alongside: WebDriverAgent reads the app in front, and reading one that
578
+ # is being terminated or uninstalled held a reinstall for 10 s (smoke, 28 Sep).
579
+ self._sweep_alerts(target)
580
+
581
+ def _reset_data(self, target, bundle_id):
582
+ udid = target.udid
583
+ result, grouped, _ = self._simctl_all([("get_app_container", udid, bundle_id, "data"),
584
+ ("get_app_container", udid, bundle_id, "groups"),
585
+ ("terminate", udid, bundle_id)])
586
+ data = result.out.strip() if result.ok else ""
587
+ if data in ("", "(null)"):
588
+ raise SimError("install", f"{bundle_id} isn't installed on the simulator. Pass --app.",
589
+ "Pass the .app built for the iOS Simulator with --app.")
590
+ groups = simctl.parse_groups(grouped.out if grouped.ok else "")
591
+ containers = [data] + [path for _, path in groups]
592
+ for container in containers: # every path is checked before anything changes
593
+ apps.guard_container(container, udid)
594
+ # cfprefsd caches preferences: deleting the plist alone would leave them alive until it restarts.
595
+ calls = [("spawn", udid, "defaults", "delete", domain)
596
+ for domain in [bundle_id] + [group for group, _ in groups if group]]
597
+ calls += [("privacy", udid, "reset", "all", bundle_id), ("keychain", udid, "reset")]
598
+ from concurrent.futures import ThreadPoolExecutor
599
+ with ThreadPoolExecutor(max_workers=1, thread_name_prefix="mobster-reset") as pool:
600
+ pending = pool.submit(self._simctl_all, calls)
601
+ for container in containers:
602
+ apps.empty_container(container, udid)
603
+ pending.result()
604
+
605
+ def _reinstall(self, target, bundle_id, app_path):
606
+ if not app_path:
607
+ raise SimError("install", f"Reinstalling {bundle_id} needs its .app.",
608
+ "Pass the .app built for the iOS Simulator with --app.")
609
+ info = self.app_info(app_path) # a device build fails here, before the installed app is removed
610
+ for bundle in dict.fromkeys((bundle_id, info.bundle_id)):
611
+ self.simctl("uninstall", target.udid, bundle, timeout=120)
612
+ self.install(target, app_path)
613
+ self._privacy_and_keychain(target.udid, info.bundle_id)
614
+
615
+ def _privacy_and_keychain(self, udid, bundle_id):
616
+ # The keychain reset covers the whole simulator, which is fine: the simulator is Mobster's own.
617
+ self._simctl_all([("privacy", udid, "reset", "all", bundle_id), ("keychain", udid, "reset")])
618
+
619
+ def launch(self, target: SimTarget, bundle_id: str, args=(), env: Optional[dict] = None) -> None:
620
+ tools = self.tools()
621
+ child = dict(tools.env)
622
+ for key, value in (env or {}).items():
623
+ child[f"SIMCTL_CHILD_{key}"] = str(value)
624
+ result = self.run([XCRUN, "simctl", "launch", "--terminate-running-process", target.udid, bundle_id,
625
+ *map(str, args)], timeout=60, env=child)
626
+ if not result.ok:
627
+ raise SimError("launch", f"{bundle_id} didn't launch: {result.last_line() or 'simctl failed'}",
628
+ "Check that the app is installed and starts in the iOS Simulator.")
629
+
630
+ def terminate(self, target: SimTarget, bundle_id: str) -> None:
631
+ self.simctl("terminate", target.udid, bundle_id, timeout=30) # "not running" is fine
632
+
633
+ def open_url(self, target: SimTarget, url: str) -> None:
634
+ scheme = urlsplit(url).scheme.lower()
635
+ approved = self._link_approved(target.udid, scheme)
636
+ result = self.simctl("openurl", target.udid, url, timeout=60)
637
+ if not result.ok:
638
+ raise SimError("launch", f"{url} didn't open: {result.last_line() or 'simctl failed'}",
639
+ "Check that the app declares the URL scheme in its Info.plist.")
640
+ if approved:
641
+ return
642
+ wda_url, started = target.wda_url, self.clock()
643
+ answered, text = self.wda.read_alert(wda_url)
644
+ # A read the runner didn't answer (it can take 10 s on a loaded Mac) doesn't end the wait early.
645
+ while text is None and (self.clock() < started + OPEN_PROMPT_WAIT or not answered) \
646
+ and self.clock() < started + OPEN_PROMPT_LIMIT:
647
+ self.sleep(ALERT_POLL)
648
+ answered, text = self.wda.read_alert(wda_url)
649
+ if text is None:
650
+ return
651
+ if not text.startswith(OPEN_PROMPT):
652
+ self._log(f"open_url {url}: left the alert {text!r} for the run")
653
+ return
654
+ for _ in range(2):
655
+ self.wda.alert_action(wda_url, "accept", OPEN_BUTTON)
656
+ if self._alert_after(wda_url, text) != text:
657
+ self._log(f"open_url {url}: pressed {OPEN_BUTTON} on {text!r}")
658
+ if scheme:
659
+ self._approved.setdefault(target.udid, set()).add(scheme) # iOS remembers it too
660
+ return
661
+ raise SimError("launch", f"{url} didn't open: iOS's “{text}” prompt stayed up after Mobster pressed "
662
+ f"{OPEN_BUTTON}.", "Run the check again.")
663
+
664
+ def _link_approved(self, udid, scheme):
665
+ """True when iOS already has Open as the answer for links of this scheme opened by `simctl openurl`."""
666
+ if not scheme:
667
+ return False
668
+ if scheme in self._approved.get(udid, ()):
669
+ return True
670
+ return bool(apps.scheme_approvals(udid).get(f"{URL_OPENER}-->{scheme}"))
671
+
672
+ def _alert_after(self, wda_url, text):
673
+ end = self.clock() + ALERT_GONE_WAIT
674
+ while True:
675
+ seen = self.wda.alert_text(wda_url)
676
+ if seen != text or self.clock() >= end:
677
+ return seen
678
+ self.sleep(ALERT_POLL)
679
+
680
+ def _sweep_alerts(self, target):
681
+ wda_url = target.wda_url
682
+ text = self.wda.alert_text(wda_url)
683
+ for _ in range(SWEEP_ROUNDS):
684
+ if text is None:
685
+ return
686
+ self._say(f"Dismissing an alert left on {target.name}: “{text}”")
687
+ self.wda.alert_action(wda_url, "dismiss")
688
+ text = self._alert_after(wda_url, text)
689
+ if text is not None:
690
+ raise SimError("simulator", f"An alert stays on {target.name}'s screen: “{text}”.",
691
+ f"Run the command again; if it stays, erase the simulator with "
692
+ f"`mobster sim erase {target.udid}`.")
693
+
694
+ def screenshot(self, target: SimTarget, path, *, max_width: Optional[int] = None, quality: int = 75) -> Path:
695
+ """A JPEG of the screen through `simctl io` (never through WDA), scaled down to ``max_width``; a .png path
696
+ gets the whole screen as a PNG."""
697
+ from PIL import Image
698
+ path = Path(path)
699
+ path.parent.mkdir(parents=True, exist_ok=True)
700
+ raw = path.with_name(f".{path.stem}.{os.getpid()}.{threading.get_ident()}.png")
701
+ try:
702
+ result = simctl.screenshot(target.udid, raw, timeout=30, env=self.tools().env, runner=self.run)
703
+ if not result.ok:
704
+ self._log(f"simctl io {target.udid} screenshot: exit {result.code}: {result.last_line()}")
705
+ raise SimError("simulator", f"The screenshot failed: {result.last_line() or 'no image'}",
706
+ "Check that the simulator is booted (`mobster sim list`).")
707
+ with Image.open(raw) as image:
708
+ image = image.convert("RGB")
709
+ if path.suffix.lower() == ".png": # a baseline compares pixels: lossless, at full size
710
+ image.save(path, "PNG")
711
+ return path
712
+ if max_width and image.width > max_width:
713
+ height = max(1, round(image.height * max_width / image.width))
714
+ image = image.resize((int(max_width), height), Image.LANCZOS)
715
+ image.save(path, "JPEG", quality=int(quality), optimize=True)
716
+ finally:
717
+ try:
718
+ raw.unlink()
719
+ except OSError:
720
+ pass
721
+ return path
722
+
723
+ def evidence_tools(self, target: SimTarget):
724
+ from ..verify.evidence import SimulatorTools
725
+ return SimulatorTools(target.udid)
726
+
727
+ def restarter(self, target: SimTarget) -> Callable:
728
+ return SimulatorWDA(target.udid, _port(target.wda_url), _port(target.mjpeg_url), target.xctestrun,
729
+ log_path=str(self.wda.runner_log(target.udid)), tools=self.tools())
730
+
731
+ # -- housekeeping --------------------------------------------------------------------------------------
732
+
733
+ def biometric(self, udid: str, action: str, *, biometric_type: str = "face-id") -> dict:
734
+ """Inspect enrollment or send one biometric test event to an idle, booted Mobster simulator."""
735
+ from .biometrics import perform
736
+ return perform(self, udid, action, biometric_type)
737
+
738
+ def _in_use(self, entry):
739
+ lease = self._try_lease(wda_url(entry["wda_port"]))
740
+ if lease is None:
741
+ return True
742
+ lease.close()
743
+ return False
744
+
745
+ def release_mine(self) -> list:
746
+ """Ask every simulator held by an agent started from the same app as this process to let go.
747
+
748
+ An agent's `mobster mcp` and the shell it runs this in share a parent chain. Returns the UDIDs asked."""
749
+ mine = set(holders.ancestors())
750
+ asked = []
751
+ for entry in self.registry.read():
752
+ record = holders.read(self.data_dir, entry["udid"])
753
+ if record is not None and record.get("ppid") in mine:
754
+ holders.request_release(self.data_dir, entry["udid"])
755
+ asked.append(entry["udid"])
756
+ return asked
757
+
758
+ def list(self) -> list:
759
+ entries = self.registry.read()
760
+ if not entries:
761
+ return []
762
+ present = {item.udid: item for item in self._devices()}
763
+ rows = []
764
+ for entry in entries:
765
+ device = present.get(entry["udid"])
766
+ state = device.state if device else "Missing"
767
+ url = wda_url(entry["wda_port"])
768
+ in_use = self._in_use(entry) if device else False
769
+ record = holders.read(self.data_dir, entry["udid"]) if in_use else None
770
+ holder = {key: record.get(key) for key in ("client", "session", "pid", "since")} if record else None
771
+ rows.append({"udid": entry["udid"], "name": entry["name"], "device_type": entry["device_type"],
772
+ "runtime": entry["runtime"], "state": state,
773
+ "wda": "ready" if state == "Booted" and self.wda.ready(url) else "stopped",
774
+ "wda_url": url, "mjpeg_url": wda_url(entry["mjpeg_port"]),
775
+ "in_use": in_use, "holder": holder,
776
+ "last_used_at": entry.get("last_used_at")})
777
+ return rows
778
+
779
+ def _entry(self, udid, entries=None):
780
+ entries = self.registry.read() if entries is None else entries
781
+ entry = next((entry for entry in entries if entry["udid"].upper() == str(udid).strip().upper()), None)
782
+ if entry is None:
783
+ raise SimError("simulator", f"{udid} isn't one of Mobster's simulators, so Mobster won't change it.",
784
+ "See Mobster's simulators with `mobster sim list`.")
785
+ return entry
786
+
787
+ def _stop_one(self, entry, state):
788
+ """Stop the runner and shut the simulator down (its lease held by the caller)."""
789
+ udid = entry["udid"]
790
+ self.wda.stop(udid)
791
+ if state not in ("Shutdown", "Missing"):
792
+ result = self.simctl("shutdown", udid, timeout=120)
793
+ if not result.ok and "current state: Shutdown" not in result.out + result.err:
794
+ raise SimError("simulator", f"{entry['name']} didn't shut down: {result.last_line()}",
795
+ "Try again in a moment.")
796
+
797
+ def _with_lease(self, entry, action, strict):
798
+ lease = self._try_lease(wda_url(entry["wda_port"]))
799
+ if lease is None:
800
+ if strict:
801
+ raise SimError("busy", f"{entry['name']} is running a check.",
802
+ "Wait for it to finish, or stop it, then try again.")
803
+ self._say(f"Skipped {entry['name']}: it is running a check")
804
+ return False
805
+ try:
806
+ action()
807
+ finally:
808
+ lease.close()
809
+ return True
810
+
811
+ def shutdown(self, udid: Optional[str] = None) -> None:
812
+ """Stop WebDriverAgent and shut down one Mobster simulator, or every one that isn't running a check."""
813
+ self.tools()
814
+ entries = [self._entry(udid)] if udid else self.registry.read()
815
+ states = {item.udid: item.state for item in self._devices()} if entries else {}
816
+ for entry in entries:
817
+ state = states.get(entry["udid"], "Missing")
818
+ if self._with_lease(entry, lambda entry=entry, state=state: self._stop_one(entry, state), bool(udid)):
819
+ if state not in ("Shutdown", "Missing"):
820
+ self._say(f"Shut down {entry['name']}")
821
+
822
+ def _guard_owned(self, entry, devices):
823
+ current = next((item for item in devices if item.udid == entry["udid"]), None)
824
+ names = [entry["name"]] + ([current.name] if current else [])
825
+ if not all(simctl.is_mobster_name(name) for name in names):
826
+ shown = current.name if current else entry["name"]
827
+ raise SimError("simulator", f"{entry['udid']} is named “{shown}”, not a Mobster simulator name, so "
828
+ "Mobster won't change it.",
829
+ "Rename it back, or delete it yourself in Xcode.")
830
+ return current
831
+
832
+ def erase(self, udid: str) -> None:
833
+ """Shut down and erase one Mobster simulator; the next acquire boots it fresh."""
834
+ self.tools()
835
+ entry = self._entry(udid)
836
+ current = self._guard_owned(entry, self._devices())
837
+ if current is None:
838
+ raise SimError("simulator", f"{entry['name']} no longer exists.", "Run `mobster sim prune`.")
839
+
840
+ def erase_it():
841
+ self._stop_one(entry, current.state)
842
+ result = self.simctl("erase", entry["udid"], timeout=120)
843
+ self._approved.pop(entry["udid"], None) # erasing forgets iOS's answers too
844
+ if not result.ok:
845
+ raise SimError("simulator", f"{entry['name']} wasn't erased: {result.last_line()}",
846
+ "Try again in a moment.")
847
+ self._say(f"Erased {entry['name']}")
848
+ self._with_lease(entry, erase_it, True)
849
+
850
+ def delete(self, udid: Optional[str] = None) -> list:
851
+ self.tools()
852
+ entries = self.registry.read()
853
+ devices = self._devices() if entries else []
854
+ chosen = [self._entry(udid, entries)] if udid else list(entries)
855
+ deleted = []
856
+ for entry in chosen:
857
+ try:
858
+ current = self._guard_owned(entry, devices)
859
+ except SimError:
860
+ if udid:
861
+ raise
862
+ self._say(f"Skipped {entry['udid']}: its name no longer starts with “Mobster · ”")
863
+ continue
864
+
865
+ def delete_it(entry=entry, current=current):
866
+ if current is not None:
867
+ self._stop_one(entry, current.state)
868
+ result = self.simctl("delete", entry["udid"], timeout=120)
869
+ if not result.ok:
870
+ raise SimError("simulator", f"{entry['name']} wasn't deleted: {result.last_line()}",
871
+ "Try again in a moment.")
872
+ deleted.append(entry["udid"])
873
+ self._say(f"Deleted {entry['name']}")
874
+ self._forget(entry["udid"])
875
+ self._with_lease(entry, delete_it, bool(udid))
876
+ return deleted
877
+
878
+ def _forget(self, udid):
879
+ self._approved.pop(udid, None)
880
+ with self.registry.edit() as entries:
881
+ entries[:] = [entry for entry in entries if entry["udid"] != udid]
882
+ try:
883
+ self.wda.runner_log(udid).unlink()
884
+ except OSError:
885
+ pass
886
+
887
+ def prune(self) -> dict:
888
+ import shutil
889
+ tools = self.tools()
890
+ removed = {"simulators": [], "entries": [], "wda_builds": []}
891
+ with self.registry.edit() as entries:
892
+ present = {item.udid for item in self._devices()} # under the lock, as in _claim
893
+ for entry in [entry for entry in entries if entry["udid"] not in present]:
894
+ entries.remove(entry)
895
+ removed["entries"].append(entry["udid"])
896
+ cutoff = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=PRUNE_DAYS)
897
+ for entry in self.registry.read():
898
+ used = _parse_time(entry.get("last_used_at")) or _parse_time(entry.get("created_at"))
899
+ if used is not None and used < cutoff:
900
+ try:
901
+ removed["simulators"] += self.delete(entry["udid"])
902
+ except SimError as exc:
903
+ self._say(f"Kept {entry['name']}: {exc}")
904
+ installed = xcode.installed_builds() | {tools.build}
905
+ running = self.wda.ps()
906
+ from .wda import COMMIT8, PATCH_DIGEST, parse_build_dir
907
+ folder = self.data_dir / "wda"
908
+ for path in sorted(folder.glob("build-*")) if folder.is_dir() else ():
909
+ parts = parse_build_dir(path.name)
910
+ stale = parts is None or parts[0] not in installed or parts[1:] != (COMMIT8, PATCH_DIGEST)
911
+ if stale and str(path) not in running:
912
+ shutil.rmtree(path, ignore_errors=True)
913
+ removed["wda_builds"].append(str(path))
914
+ for path in sorted(folder.glob("src-*")) if folder.is_dir() else ():
915
+ if path.name != f"src-{COMMIT8}":
916
+ shutil.rmtree(path, ignore_errors=True)
917
+ removed["wda_builds"].append(str(path))
918
+ self._log(f"pruned {removed}")
919
+ return removed
920
+
921
+ def status(self) -> dict:
922
+ try:
923
+ tools = self.tools()
924
+ xcode = {"version": tools.version, "build": tools.build,
925
+ "path": str(Path(tools.xcodebuild).parents[4])}
926
+ except SimError as exc:
927
+ tools, xcode = None, {"error": str(exc)}
928
+ runtime = device_type = None
929
+ simulators = []
930
+ if tools is not None:
931
+ try:
932
+ chosen_type, chosen_runtime, _ = self._choose()
933
+ runtime, device_type = chosen_runtime.name, chosen_type.name
934
+ except SimError as exc:
935
+ runtime = {"error": str(exc)}
936
+ try:
937
+ simulators = self.list()
938
+ except SimError as exc:
939
+ simulators = [{"error": str(exc)}]
940
+ plan = find_xctestrun(build_dir(self.data_dir, tools.build)) if tools else None
941
+ return {"xcode": xcode, "runtime": runtime, "device_type": device_type,
942
+ "wda_build": {"cached": plan is not None, "path": str(plan) if plan else None},
943
+ "simulators": simulators, "data_dir": str(self.data_dir), "max_sims": self.max_sims}
944
+
945
+ def doctor(self, fix: bool = False) -> list:
946
+ from .doctor import run_checks
947
+ return run_checks(self, fix=fix)
948
+
949
+
950
+ def _executable(info):
951
+ name = info.get("CFBundleExecutable") if isinstance(info, dict) else None
952
+ return name.strip() if isinstance(name, str) and name.strip() else None