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,618 @@
1
+ """WebDriverAgent on Mobster's simulators: the pinned source, one build per Xcode, and one runner per simulator."""
2
+
3
+ import atexit
4
+ import contextlib
5
+ import errno
6
+ import fcntl
7
+ import hashlib
8
+ import json
9
+ import os
10
+ import re
11
+ import shutil
12
+ import signal
13
+ import subprocess
14
+ import threading
15
+ import time
16
+ import urllib.error
17
+ import urllib.request
18
+ from pathlib import Path
19
+ from typing import NamedTuple
20
+
21
+ from ..devices import LOOPBACK, wda_url
22
+ from ..wda import gate as wda_gate
23
+ from ..wda import patches as wda_patches
24
+ from ..wda import source as wda_source
25
+ from ..wda.patches import pin_loopback, xctestrun_targets
26
+ from .models import SimError
27
+ from .simctl import run as run_command
28
+
29
+ COMMIT8 = wda_source.COMMIT[:8]
30
+ PATCH_DIGEST = hashlib.sha256(repr(wda_patches.active_patches()).encode()).hexdigest()[:8] # the flagged routes too
31
+ BUILD_TIMEOUT = 1200
32
+ CLONE_TIMEOUT = 600
33
+ READY_TIMEOUT = 240 # a busy Mac took over 120 s to start a runner (smoke, 28 Sep, load average 780)
34
+ READY_POLL = .5
35
+ BUILD_LOCK_TIMEOUT = 1500
36
+ MJPEG_WAIT = 5.0 # the MJPEG socket opens a moment after /status answers
37
+ HTTP_TIMEOUT = 10.0
38
+ DEVICE_PATH = re.compile(r"/CoreSimulator/Devices/([0-9A-Fa-f-]{36})/")
39
+ HEARTBEAT_SECONDS = 30.0
40
+ STOP_GRACE_SECONDS = 1 # the runner is dead or wedged: its xcodebuild is not waited on long
41
+
42
+
43
+ def elapsed_words(seconds):
44
+ """"45 s", "2 min 05 s"."""
45
+ seconds = int(max(0, seconds))
46
+ return f"{seconds} s" if seconds < 60 else f"{seconds // 60} min {seconds % 60:02d} s"
47
+
48
+
49
+ @contextlib.contextmanager
50
+ def heartbeat(progress, what, interval=None, clock=time.monotonic):
51
+ interval = HEARTBEAT_SECONDS if interval is None else interval
52
+ done = threading.Event()
53
+ started = clock()
54
+
55
+ def beat():
56
+ while not done.wait(interval):
57
+ try:
58
+ progress(f"Still {what} ({elapsed_words(clock() - started)} so far)")
59
+ except Exception:
60
+ return
61
+ thread = threading.Thread(target=beat, name="mobster-heartbeat", daemon=True)
62
+ thread.start()
63
+ try:
64
+ yield
65
+ finally:
66
+ done.set()
67
+
68
+
69
+ # Every build or clone ``run_logged`` has running, across WebDriverAgent instances. Each runs in its own process
70
+ # group (start_new_session), so neither ctrl+c nor the end of this process reaches it: the exit hook below kills
71
+ # what is left, which covers a build on a daemon thread when the interpreter exits under it.
72
+ _RUNNING = set()
73
+
74
+
75
+ @atexit.register
76
+ def _kill_running():
77
+ for process in list(_RUNNING):
78
+ _kill_group(process)
79
+ _RUNNING.clear()
80
+
81
+
82
+ # The simulator runners this process started (xcodebuild test-without-building), each in its own session so a
83
+ # crash or ctrl+c of a command leaves the warm simulator's runner for the next command. A process that owns them for
84
+ # its whole life (`mobster serve`) calls stop_started() as it closes, or the runner outlives it and boots the
85
+ # simulator again after `simctl shutdown`.
86
+ _STARTED = set()
87
+
88
+
89
+ def stop_started():
90
+ """Stop every runner this process started and that is still running (their process groups)."""
91
+ for process in list(_STARTED):
92
+ _STARTED.discard(process)
93
+ if process.poll() is None:
94
+ _kill_group(process)
95
+
96
+
97
+ def source_dir(data_dir):
98
+ return Path(data_dir) / "wda" / f"src-{COMMIT8}"
99
+
100
+
101
+ def build_dir(data_dir, xcode_build):
102
+ return Path(data_dir) / "wda" / f"build-{xcode_build}-{COMMIT8}-{PATCH_DIGEST}"
103
+
104
+
105
+ def parse_build_dir(name):
106
+ """(Xcode build, commit8, patch digest) from a build folder's name, or None."""
107
+ match = re.fullmatch(r"build-([^-]+)-([0-9a-f]{8})-([0-9a-f]{8})", name)
108
+ return match.groups() if match else None
109
+
110
+
111
+ def http_json(method, url, body=None, timeout=HTTP_TIMEOUT):
112
+ data = None if body is None else json.dumps(body).encode()
113
+ request = urllib.request.Request(url, data=data, method=method,
114
+ headers={"Content-Type": "application/json"} if data is not None else {})
115
+ opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
116
+ try:
117
+ with opener.open(request, timeout=timeout) as response:
118
+ status, raw = response.status, response.read()
119
+ except urllib.error.HTTPError as exc:
120
+ try:
121
+ status, raw = exc.code, exc.read()
122
+ finally:
123
+ exc.close()
124
+ except (OSError, ValueError):
125
+ return None, None
126
+ try:
127
+ payload = json.loads(raw.decode("utf-8", "replace") or "null")
128
+ except ValueError:
129
+ payload = None
130
+ return status, (payload.get("value") if isinstance(payload, dict) else payload)
131
+
132
+
133
+ def find_xctestrun(build):
134
+ """The simulator test plan a finished build leaves, or None."""
135
+ products = Path(build) / "Build" / "Products"
136
+ try:
137
+ plans = sorted(products.glob("*iphonesimulator*.xctestrun"), key=lambda path: path.stat().st_mtime)
138
+ except OSError:
139
+ return None
140
+ return plans[-1] if plans else None
141
+
142
+
143
+ # -- lsof and ps -------------------------------------------------------------------------------------------
144
+
145
+ class Listener(NamedTuple):
146
+ command: str
147
+ pid: int
148
+ host: str # "127.0.0.1", "*", "::1", "192.168.1.4"
149
+ port: int
150
+
151
+
152
+ def parse_lsof(text):
153
+ """Listening sockets from `lsof -nP -iTCP:<port> -sTCP:LISTEN` (its default table)."""
154
+ listeners = []
155
+ for line in (text or "").splitlines():
156
+ tokens = line.split()
157
+ if len(tokens) < 9 or tokens[0] == "COMMAND" or not tokens[1].isdigit():
158
+ continue
159
+ name = tokens[-2] if tokens[-1] == "(LISTEN)" else tokens[-1]
160
+ host, _, port = name.rpartition(":")
161
+ if not port.isdigit():
162
+ continue
163
+ host = host[1:-1] if host.startswith("[") and host.endswith("]") else host
164
+ listeners.append(Listener(tokens[0], int(tokens[1]), host or "*", int(port)))
165
+ return listeners
166
+
167
+
168
+ def loopback_only(listeners):
169
+ """True when something listens, and only on 127.0.0.1."""
170
+ return bool(listeners) and all(listener.host == LOOPBACK for listener in listeners)
171
+
172
+
173
+ def device_of(command):
174
+ """The UDID a simulator process runs in, from its executable's path, or None."""
175
+ match = DEVICE_PATH.search(command or "")
176
+ return match.group(1).upper() if match else None
177
+
178
+
179
+ def runner_commands(udid, ps_output):
180
+ """[(pid, command)] of the xcodebuild runs driving this simulator's runner."""
181
+ runners = []
182
+ for line in (ps_output or "").splitlines():
183
+ pid, _, command = line.strip().partition(" ")
184
+ if ("xcodebuild" in command and "test-without-building" in command and f"id={udid}" in command
185
+ and pid.isdigit()):
186
+ runners.append((int(pid), command.strip()))
187
+ return runners
188
+
189
+
190
+ def runner_pids(udid, ps_output=None):
191
+ """PIDs of the xcodebuild test runs whose destination is this simulator (`ps` is read unless given)."""
192
+ if ps_output is None:
193
+ ps_output = subprocess.run(["ps", "-axo", "pid=,command="], capture_output=True, text=True,
194
+ timeout=10).stdout
195
+ return [pid for pid, _ in runner_commands(udid, ps_output)]
196
+
197
+
198
+ def runner_command(xcodebuild, xctestrun, udid):
199
+ """The command that runs a built runner on one simulator."""
200
+ return [xcodebuild, "test-without-building", "-xctestrun", str(xctestrun), "-destination", f"id={udid}"]
201
+
202
+
203
+ def runner_env(base, port, mjpeg):
204
+ """``base`` plus the runner's ports, 127.0.0.1 only whatever the test plan says, and the actuation gate."""
205
+ return {**base, "TEST_RUNNER_USE_PORT": str(port), "TEST_RUNNER_MJPEG_SERVER_PORT": str(mjpeg),
206
+ "TEST_RUNNER_USE_IP": LOOPBACK, **wda_gate.runner_env("simulator")}
207
+
208
+
209
+ def ready(url, timeout=2):
210
+ """True when the runner at ``url`` answers /status as ready."""
211
+ try:
212
+ with urllib.request.urlopen(url.rstrip("/") + "/status", timeout=timeout) as response:
213
+ value = json.load(response).get("value") or {}
214
+ return value.get("ready", True) is not False
215
+ except (OSError, ValueError, AttributeError):
216
+ return False
217
+
218
+
219
+ def _alive(pid):
220
+ try:
221
+ os.kill(pid, 0)
222
+ except OSError:
223
+ return False
224
+ return True
225
+
226
+
227
+ def _signal(pid, number):
228
+ # a runner started by hand shares its shell's process group with other simulators' runners (26 Sep)
229
+ try:
230
+ if os.getpgid(pid) == pid:
231
+ os.killpg(pid, number)
232
+ else:
233
+ os.kill(pid, number)
234
+ except OSError:
235
+ pass
236
+
237
+
238
+ def terminate(pids, clock=time.monotonic, sleep=time.sleep):
239
+ """SIGTERM these runners, give them STOP_GRACE_SECONDS, then SIGKILL what is left; returns ``pids``."""
240
+ for pid in pids:
241
+ _signal(pid, signal.SIGTERM)
242
+ end = clock() + STOP_GRACE_SECONDS
243
+ while clock() < end and any(_alive(pid) for pid in pids):
244
+ sleep(.2)
245
+ for pid in pids:
246
+ if _alive(pid):
247
+ _signal(pid, signal.SIGKILL)
248
+ return pids
249
+
250
+
251
+ def xctestrun_of(command):
252
+ """The test plan an xcodebuild runner was started with (paths may hold spaces)."""
253
+ match = re.search(r" -xctestrun (.+?\.xctestrun)(?= |$)", command or "")
254
+ return match.group(1) if match else None
255
+
256
+
257
+ REUSE, RESTART, MOVE, START = "reuse", "restart", "move", "start"
258
+ UNCHECKED = "unchecked" # the owner lookup (lsof) was skipped: this simulator's own runner answers
259
+
260
+
261
+ def runner_plan(*, udid, listening, owner, ready, runners, xctestrun):
262
+ current = any(xctestrun_of(command) == str(xctestrun) for _, command in runners)
263
+ if listening and ready and current and owner in (udid, UNCHECKED):
264
+ return REUSE
265
+ # something else listens on the port: take another pair, never adopt a stranger's server
266
+ if listening and owner not in (udid, UNCHECKED):
267
+ return MOVE
268
+ if listening:
269
+ return RESTART
270
+ return RESTART if runners else START
271
+
272
+
273
+ class WebDriverAgent:
274
+ """Build, start, stop and check simulator runners."""
275
+
276
+ SEAMS = ("run", "ready", "listening", "popen", "sleep", "clock", "http")
277
+
278
+ def __init__(self, data_dir, *, log=None, progress=None):
279
+ from .registry import listening
280
+ self.data_dir = Path(data_dir)
281
+ self.logs = self.data_dir / "logs"
282
+ self.log = log or (lambda message: None)
283
+ self.progress = progress or (lambda message: None)
284
+ self.run = run_command
285
+ self.ready = ready
286
+ self.listening = listening
287
+ self.popen = subprocess.Popen
288
+ self.sleep = time.sleep
289
+ self.clock = time.monotonic
290
+ self.http = http_json
291
+ self._running = set()
292
+
293
+ def ps(self):
294
+ return self.run(["/bin/ps", "-axo", "pid=,command="], timeout=10).out
295
+
296
+ def lsof(self, port):
297
+ return self.run(["/usr/sbin/lsof", "-nP", f"-iTCP:{int(port)}", "-sTCP:LISTEN"], timeout=10).out
298
+
299
+ # -- source and build ----------------------------------------------------------------------------------
300
+
301
+ @contextlib.contextmanager
302
+ def build_lock(self):
303
+ """One build or clone at a time across processes; the second waits and then finds it built."""
304
+ folder = self.data_dir / "wda"
305
+ folder.mkdir(parents=True, exist_ok=True)
306
+ fd = os.open(folder / "build.lock", os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
307
+ try:
308
+ end, told = self.clock() + BUILD_LOCK_TIMEOUT, False
309
+ while True:
310
+ try:
311
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
312
+ break
313
+ except OSError as exc:
314
+ if exc.errno not in (errno.EWOULDBLOCK, errno.EAGAIN) or self.clock() >= end:
315
+ raise SimError("busy", "Another Mobster process is still building WebDriverAgent.",
316
+ "Wait for it to finish, then try again.") from None
317
+ if not told:
318
+ self.progress("Waiting for another Mobster process to finish building WebDriverAgent")
319
+ told = True
320
+ self.sleep(1)
321
+ yield
322
+ finally:
323
+ os.close(fd)
324
+
325
+ def ensure_source(self, tools):
326
+ """The pinned clone, patched. Cloned into a temporary folder and renamed, so a half clone never counts."""
327
+ source = source_dir(self.data_dir)
328
+ if not tools.git:
329
+ raise SimError("environment", "git isn't installed, and building WebDriverAgent needs it.",
330
+ "Install Xcode, which includes git.")
331
+ if source.is_dir() and wda_source.is_pinned(tools.git, source):
332
+ self._patch(source)
333
+ return source
334
+ if source.exists():
335
+ shutil.rmtree(source)
336
+ temporary = source.with_name(f".{source.name}-{os.getpid()}")
337
+ if temporary.exists():
338
+ shutil.rmtree(temporary)
339
+ self.progress(f"Downloading WebDriverAgent {wda_source.REF}")
340
+ with heartbeat(self.progress, "downloading WebDriverAgent"):
341
+ result = self.run(wda_source.clone_command(tools.git, temporary), timeout=CLONE_TIMEOUT, env=tools.env)
342
+ if not result.ok:
343
+ shutil.rmtree(temporary, ignore_errors=True)
344
+ raise SimError("wda", f"Couldn't download WebDriverAgent {wda_source.REF} from GitHub: "
345
+ f"{result.last_line() or 'git failed'}",
346
+ "Check this Mac's network connection, then try again.")
347
+ try:
348
+ wda_source.verify(tools.git, temporary)
349
+ except RuntimeError as exc:
350
+ shutil.rmtree(temporary, ignore_errors=True)
351
+ raise SimError("wda", str(exc), "Try again; if it repeats, the release was moved upstream.") from None
352
+ os.replace(temporary, source)
353
+ self._patch(source)
354
+ return source
355
+
356
+ def _patch(self, source):
357
+ try:
358
+ wda_patches.patch_wda(source)
359
+ except (OSError, RuntimeError):
360
+ raise SimError("wda", "WebDriverAgent's source doesn't take Mobster's loopback patches.",
361
+ f"Delete {source} and try again.") from None
362
+
363
+ def ensure_build(self, tools):
364
+ build = build_dir(self.data_dir, tools.build)
365
+ plan = find_xctestrun(build)
366
+ if plan and plan_is_pinned(plan):
367
+ return plan
368
+ with self.build_lock():
369
+ plan = find_xctestrun(build)
370
+ if plan:
371
+ # a process that died between xcodebuild writing the plan and the pin left it listening everywhere
372
+ if not plan_is_pinned(plan):
373
+ pin_loopback(plan.parent)
374
+ self.log(f"pinned the cached test plan to {LOOPBACK}: {plan}")
375
+ return plan
376
+ source = self.ensure_source(tools)
377
+ self.logs.mkdir(parents=True, exist_ok=True)
378
+ log = self.logs / "wda-build.log"
379
+ self.progress(f"Building WebDriverAgent for Xcode {tools.version}, once per Xcode version: usually 2 to "
380
+ f"5 minutes (log: {log})")
381
+ command = [tools.xcodebuild, "build-for-testing", "-project", str(source / "WebDriverAgent.xcodeproj"),
382
+ "-scheme", "WebDriverAgentRunner", "-destination", "generic/platform=iOS Simulator",
383
+ "-derivedDataPath", str(build), "CODE_SIGNING_ALLOWED=NO"]
384
+ started = self.clock()
385
+ with heartbeat(self.progress, "building WebDriverAgent"):
386
+ code = self.run_logged(command, log, BUILD_TIMEOUT, tools.env)
387
+ plan = find_xctestrun(build)
388
+ if code != 0 or not plan:
389
+ raise SimError("wda", f"WebDriverAgent didn't build: {last_error(log) or 'xcodebuild failed'}",
390
+ f"See {log}.")
391
+ pin_loopback(build / "Build" / "Products")
392
+ self.log(f"built WebDriverAgent in {self.clock() - started:.1f} s: {plan}")
393
+ return plan
394
+
395
+ def run_logged(self, command, log, timeout, env):
396
+ with open(log, "ab") as stream:
397
+ stream.write(f"\n$ {' '.join(map(str, command))}\n".encode())
398
+ stream.flush()
399
+ try:
400
+ process = subprocess.Popen(command, stdout=stream, stderr=subprocess.STDOUT,
401
+ stdin=subprocess.DEVNULL, env=env, start_new_session=True)
402
+ except OSError as exc:
403
+ stream.write(f"\nerror: {exc}\n".encode())
404
+ return None
405
+ self._running.add(process)
406
+ _RUNNING.add(process)
407
+ try:
408
+ return process.wait(timeout=timeout)
409
+ except subprocess.TimeoutExpired:
410
+ _kill_group(process)
411
+ stream.write(f"\nerror: did not finish in {timeout} s\n".encode())
412
+ return None
413
+ except BaseException:
414
+ _kill_group(process)
415
+ raise
416
+ finally:
417
+ self._running.discard(process)
418
+ _RUNNING.discard(process)
419
+
420
+ def cancel(self):
421
+ """Stop a build running in another thread (the acquire that started it failed or was interrupted)."""
422
+ for process in list(self._running):
423
+ _kill_group(process)
424
+
425
+ # -- runners -------------------------------------------------------------------------------------------
426
+
427
+ def runner_log(self, udid):
428
+ return self.logs / f"wda-{udid}.log"
429
+
430
+ def owner(self, port):
431
+ """The UDID of the simulator whose process listens on ``port``, or None."""
432
+ pids = {listener.pid for listener in parse_lsof(self.lsof(port))}
433
+ for pid in sorted(pids):
434
+ command = self.run(["/bin/ps", "-o", "command=", "-p", str(pid)], timeout=10).out
435
+ udid = device_of(command)
436
+ if udid:
437
+ return udid
438
+ return None
439
+
440
+ def plan(self, udid, port, mjpeg, xctestrun):
441
+ url = wda_url(port)
442
+ runners = runner_commands(udid, self.ps())
443
+ current = any(xctestrun_of(command) == str(xctestrun) for _, command in runners)
444
+ busy_ports = [number for number in (port, mjpeg) if self.listening(number)]
445
+ ready = port in busy_ports and self.ready(url)
446
+ owner = None
447
+ if busy_ports and ready and current:
448
+ owner = UNCHECKED
449
+ elif busy_ports:
450
+ owners = {self.owner(number) for number in busy_ports}
451
+ owner = udid if owners == {udid} else next(iter(owners - {udid})) # a stranger on either port
452
+ return runner_plan(udid=udid, listening=bool(busy_ports), owner=owner, ready=ready, runners=runners,
453
+ xctestrun=xctestrun)
454
+
455
+ def start(self, udid, port, mjpeg, xctestrun, tools):
456
+ """Start the runner in its own session, output to logs/wda-<udid>.log."""
457
+ self.logs.mkdir(parents=True, exist_ok=True)
458
+ env = runner_env(tools.env, port, mjpeg)
459
+ gate = env[wda_gate.RUNNER_VARIABLE]
460
+ command = runner_command(tools.xcodebuild, xctestrun, udid)
461
+ with open(self.runner_log(udid), "ab") as stream:
462
+ stream.write(f"\n$ TEST_RUNNER_USE_PORT={port} TEST_RUNNER_MJPEG_SERVER_PORT={mjpeg} "
463
+ f"TEST_RUNNER_USE_IP={LOOPBACK} {wda_gate.RUNNER_VARIABLE}={gate} "
464
+ f"{' '.join(command)}\n".encode())
465
+ stream.flush()
466
+ process = self.popen(command, cwd=str(Path(xctestrun).parent), env=env, stdout=stream,
467
+ stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL, start_new_session=True)
468
+ _STARTED.add(process)
469
+ return process
470
+
471
+ def wait_ready(self, udid, port, process=None, timeout=READY_TIMEOUT):
472
+ with heartbeat(self.progress, f"waiting for WebDriverAgent to answer (log: {self.runner_log(udid)})"):
473
+ return self._wait_ready(udid, port, process, timeout)
474
+
475
+ def _wait_ready(self, udid, port, process, timeout):
476
+ url = wda_url(port)
477
+ end = self.clock() + timeout
478
+ while self.clock() < end:
479
+ if self.ready(url):
480
+ return
481
+ if process is not None and process.poll() is not None:
482
+ raise SimError("wda", "WebDriverAgent stopped before it was ready: "
483
+ f"{last_error(self.runner_log(udid)) or 'xcodebuild exited'}",
484
+ f"See {self.runner_log(udid)}.")
485
+ self.sleep(READY_POLL)
486
+ raise SimError("wda", f"WebDriverAgent didn't answer on {url} within {timeout:g} s, which happens when "
487
+ "this Mac is busy.",
488
+ f"Run the command again. If it keeps happening, see {self.runner_log(udid)}.")
489
+
490
+ def stop(self, udid):
491
+ """Stop this simulator's runner and only it: SIGTERM, a short wait, SIGKILL."""
492
+ return terminate(runner_pids(udid, self.ps()), self.clock, self.sleep)
493
+
494
+ def listeners(self, port):
495
+ return parse_lsof(self.lsof(port))
496
+
497
+ # -- system alerts (sessionless) -----------------------------------------------------------------------
498
+
499
+ def read_alert(self, url):
500
+ status, value = self.http("GET", f"{url}/alert/text")
501
+ return status is not None, (value if status == 200 and isinstance(value, str) and value else None)
502
+
503
+ def alert_text(self, url):
504
+ """The text of the alert on screen, or None when there is none or the runner didn't answer."""
505
+ return self.read_alert(url)[1]
506
+
507
+ def alert_action(self, url, action, name=None):
508
+ if action not in ("accept", "dismiss"):
509
+ raise ValueError(f"alert action must be accept or dismiss, not {action!r}")
510
+ status, _ = self.http("POST", f"{url}/alert/{action}", {"name": name} if name else {})
511
+ return status == 200 # even when nothing changed: the caller reads the alert again
512
+
513
+ def check_loopback(self, port, mjpeg):
514
+ api = self.listeners(port)
515
+ end = self.clock() + MJPEG_WAIT
516
+ video = self.listeners(mjpeg)
517
+ while not video and self.clock() < end:
518
+ self.sleep(.25)
519
+ video = self.listeners(mjpeg)
520
+ return loopback_only(api) and loopback_only(video), {port: api, mjpeg: video}
521
+
522
+
523
+ def _kill_group(process):
524
+ """SIGTERM the process's group, then SIGKILL it if it is still there after two seconds."""
525
+ for number, grace in ((signal.SIGTERM, 2.0), (signal.SIGKILL, 0)):
526
+ try:
527
+ os.killpg(process.pid, number)
528
+ except OSError:
529
+ return
530
+ try:
531
+ process.wait(timeout=grace or None)
532
+ return
533
+ except subprocess.TimeoutExpired:
534
+ continue
535
+
536
+
537
+ def last_error(log, window=65536):
538
+ """The last `error:` line of a log (or its last line), for a message."""
539
+ try:
540
+ with open(log, "rb") as stream:
541
+ stream.seek(0, os.SEEK_END)
542
+ stream.seek(max(0, stream.tell() - window))
543
+ lines = stream.read().decode(errors="replace").splitlines()
544
+ except OSError:
545
+ return ""
546
+ errors = [line.strip() for line in lines if "error:" in line.lower()]
547
+ tail = errors or [line.strip() for line in lines if line.strip()]
548
+ return tail[-1][:300] if tail else ""
549
+
550
+
551
+ class SimulatorWDA:
552
+ """The Smart loop's WebDriverAgent restarter for one simulator, which never stops another simulator's runner."""
553
+
554
+ READY_POLL_SECONDS = .5
555
+
556
+ def __init__(self, udid, port, mjpeg_port, xctestrun, *, log_path=None, cwd=None, tools=None):
557
+ if not Path(xctestrun).is_file():
558
+ raise FileNotFoundError(f"no xctestrun at {xctestrun}")
559
+ self.udid, self.port, self.mjpeg_port = udid, int(port), int(mjpeg_port)
560
+ self.xctestrun, self.log_path = str(xctestrun), log_path
561
+ self.cwd = cwd or str(Path(xctestrun).resolve().parent)
562
+ self.url = wda_url(self.port)
563
+ self.tools = tools
564
+ self.restarts = 0
565
+
566
+ def command(self):
567
+ return runner_command(self.tools.xcodebuild if self.tools else "xcodebuild", self.xctestrun, self.udid)
568
+
569
+ def environment(self):
570
+ return runner_env(self.tools.env if self.tools else os.environ, self.port, self.mjpeg_port)
571
+
572
+ def stop(self):
573
+ """Stop this simulator's runner (its xcodebuild), gracefully then by force."""
574
+ return terminate(runner_pids(self.udid))
575
+
576
+ def start(self):
577
+ log = open(self.log_path, "ab") if self.log_path else subprocess.DEVNULL
578
+ try:
579
+ # Its own session: ctrl+c does not reach it. stop_started() does, for a process that owns it.
580
+ process = subprocess.Popen(self.command(), cwd=self.cwd, env=self.environment(), stdout=log,
581
+ stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL, start_new_session=True)
582
+ _STARTED.add(process)
583
+ return process
584
+ finally:
585
+ if log is not subprocess.DEVNULL:
586
+ log.close()
587
+
588
+ def __call__(self, timeout=90):
589
+ """Restart the runner; returns once it answers /status, or raises TimeoutError."""
590
+ started = time.monotonic()
591
+ self.stop()
592
+ self.timing = {"stop_ms": round((time.monotonic() - started) * 1000)}
593
+ self.start()
594
+ self.restarts += 1
595
+ end = time.monotonic() + timeout
596
+ while time.monotonic() < end:
597
+ if ready(self.url):
598
+ self.timing["ready_ms"] = round((time.monotonic() - started) * 1000)
599
+ return True
600
+ time.sleep(self.READY_POLL_SECONDS)
601
+ raise TimeoutError(f"WDA on port {self.port} did not come back in {timeout:.0f} s")
602
+
603
+
604
+ def plan_is_pinned(path):
605
+ """Every target of a test plan has USE_IP=127.0.0.1."""
606
+ import plistlib
607
+ try:
608
+ with open(path, "rb") as stream:
609
+ plan = plistlib.load(stream)
610
+ except (OSError, ValueError, plistlib.InvalidFileException):
611
+ return False
612
+ targets = xctestrun_targets(plan)
613
+ return bool(targets) and all(
614
+ isinstance(target.get("EnvironmentVariables"), dict)
615
+ and target["EnvironmentVariables"].get("USE_IP") == LOOPBACK
616
+ and (not isinstance(target.get("TestingEnvironmentVariables"), dict)
617
+ or target["TestingEnvironmentVariables"].get("USE_IP", LOOPBACK) == LOOPBACK)
618
+ for target in targets)
@@ -0,0 +1,74 @@
1
+ """The Xcode Mobster runs simulators with: its version, its build and the environment to run its tools in."""
2
+
3
+ import plistlib
4
+ import re
5
+ from pathlib import Path
6
+ from typing import NamedTuple
7
+
8
+ from .. import mac_tools
9
+ from .models import SimError
10
+
11
+
12
+ class Tools(NamedTuple):
13
+ """Xcode's tools for one run: its version and build, and the environment to run them in."""
14
+ xcodebuild: str
15
+ git: str
16
+ version: str # "26.4"
17
+ build: str # "17E192"
18
+ env: dict
19
+
20
+
21
+ def find_tools(run):
22
+ xcode, selected = mac_tools.developer_dir()
23
+ if not xcode:
24
+ raise SimError("environment", "Xcode isn't installed. Mobster runs simulators with it.",
25
+ "Install Xcode from the App Store, open it once, then run `mobster sim doctor`.")
26
+ # The Command Line Tools are selected: use the Xcode found anyway. Never the shell's settings for building for
27
+ # this Mac (mac_tools.HOST_BUILD_ENV): they break the simulator's helper build.
28
+ env = mac_tools.xcode_env(xcode if selected != xcode else None)
29
+ version, build = app_version(xcode)
30
+ xcodebuild = str(Path(xcode) / "usr" / "bin" / "xcodebuild")
31
+ if not build:
32
+ result = run([xcodebuild, "-version"], timeout=60, env=env)
33
+ version, build = parse_version(result.out)
34
+ if not result.ok or not build:
35
+ text = result.out + result.err
36
+ if "license" in text.lower():
37
+ raise SimError("environment", "Xcode's license isn't accepted yet.",
38
+ "Run `sudo xcodebuild -license accept` in Terminal.")
39
+ raise SimError("environment", f"xcodebuild didn't report its version: {result.last_line()}",
40
+ "Open Xcode once to finish its setup, then run `mobster sim doctor`.")
41
+ git = mac_tools.xcode_tool("git", developer=xcode) or ""
42
+ return Tools(xcodebuild=xcodebuild, git=git, version=version, build=build, env=env)
43
+
44
+
45
+ def app_version(developer_dir):
46
+ """("26.4", "17E192") from the Xcode app's version.plist, else ("", "")."""
47
+ try:
48
+ with open(Path(developer_dir).parent / "version.plist", "rb") as stream:
49
+ info = plistlib.load(stream)
50
+ except (OSError, ValueError, plistlib.InvalidFileException):
51
+ return "", ""
52
+ return str(info.get("CFBundleShortVersionString") or ""), str(info.get("ProductBuildVersion") or "")
53
+
54
+
55
+ def parse_version(text):
56
+ """("26.4", "17E192") from `xcodebuild -version`."""
57
+ version = re.search(r"Xcode\s+(\S+)", text or "")
58
+ build = re.search(r"Build version\s+(\S+)", text or "")
59
+ return (version.group(1) if version else ""), (build.group(1) if build else "")
60
+
61
+
62
+ def installed_builds():
63
+ """The build versions of the Xcode apps in /Applications and ~/Applications."""
64
+ builds = set()
65
+ for root in mac_tools.XCODE_APP_DIRS:
66
+ try:
67
+ candidates = list(Path(root).glob("Xcode*.app"))
68
+ except OSError:
69
+ continue
70
+ for app in candidates:
71
+ build = app_version(app / "Contents" / "Developer")[1]
72
+ if build:
73
+ builds.add(build)
74
+ return builds