@xenon-device-management/xenon 2.0.0 → 2.1.0

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 (279) hide show
  1. package/README.md +2 -1
  2. package/lib/package.json +1 -2
  3. package/lib/public/assets/{AnnotationOverlay-BmTPCAds.js → AnnotationOverlay-C-Pw_hvw.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-B5eB7f8B.js → ApiKeyGate-DmYQ0oSk.js} +1 -1
  5. package/lib/public/assets/BugReportButton-n8T1dFUp.js +10 -0
  6. package/lib/public/assets/{DeviceMosaicView-9DUoZnQi.js → DeviceMosaicView-D-rGOBbJ.js} +1 -1
  7. package/lib/public/assets/{EmptyState-lvyNiXev.js → EmptyState-DExETevp.js} +1 -1
  8. package/lib/public/assets/{FieldGroup-BduMPM9K.js → FieldGroup-CCdFlZm6.js} +1 -1
  9. package/lib/public/assets/{FilterMenu-BSqX6I94.js → FilterMenu-DiUSOSL-.js} +1 -1
  10. package/lib/public/assets/{Menu-Ba5cT7jZ.js → Menu-ZOfpH6gh.js} +1 -1
  11. package/lib/public/assets/{Modal-CceoMY7D.js → Modal-CZNtoB-1.js} +1 -1
  12. package/lib/public/assets/{RecordingPage-6T9dOidc.js → RecordingPage-BKp6h8Jl.js} +1 -1
  13. package/lib/public/assets/{RecordingsPage-qj6FdZXR.js → RecordingsPage-Bf881FX4.js} +1 -1
  14. package/lib/public/assets/{SegmentedControl-CSRuZ8TR.js → SegmentedControl-EUoyizvW.js} +1 -1
  15. package/lib/public/assets/{SettingCard-Bp1kz4By.js → SettingCard-ClT4hf4_.js} +1 -1
  16. package/lib/public/assets/{Table-LV7zFyZg.js → Table-ucR4bnmc.js} +1 -1
  17. package/lib/public/assets/{activity-D17TUloi.js → activity-K-hdylwq.js} +1 -1
  18. package/lib/public/assets/{ai-settings-qvbhY89P.js → ai-settings-BLhdwI3c.js} +1 -1
  19. package/lib/public/assets/{api-keys-BfpvdG7G.js → api-keys-BbiN7rBI.js} +1 -1
  20. package/lib/public/assets/{apps-Dbnzd0fW.js → apps-Dez1petz.js} +1 -1
  21. package/lib/public/assets/{arrow-left-DD1VjtC7.js → arrow-left-UjmFHLAJ.js} +1 -1
  22. package/lib/public/assets/{arrow-right-D0pnM3ma.js → arrow-right-BVyEigbu.js} +1 -1
  23. package/lib/public/assets/{arrow-up-right-BIC8JOV6.js → arrow-up-right-B-ZlBof3.js} +1 -1
  24. package/lib/public/assets/{auth-shell-DWHPXSnI.js → auth-shell-DAQjAgUv.js} +2 -2
  25. package/lib/public/assets/{builds-page-ClCNBUv_.js → builds-page-CmIPa5JK.js} +1 -1
  26. package/lib/public/assets/{button-xtAhYR-f.js → button-Dd-IhMC1.js} +1 -1
  27. package/lib/public/assets/{calendar-CupwJ7Bm.js → calendar-CAIeJwa5.js} +1 -1
  28. package/lib/public/assets/{check-CyuEoKkd.js → check-C4Jrjg8f.js} +1 -1
  29. package/lib/public/assets/{chevron-left-DgX-F9xK.js → chevron-left-BGzlf-4i.js} +1 -1
  30. package/lib/public/assets/{chevron-right-BKJgJnU9.js → chevron-right-_wYqDjDC.js} +1 -1
  31. package/lib/public/assets/{circle-check-CZdg8mMa.js → circle-check-Ckn00yMB.js} +1 -1
  32. package/lib/public/assets/{circle-x-CZH6t9z-.js → circle-x-pdVkomLa.js} +1 -1
  33. package/lib/public/assets/{clock-Cd-t7xpe.js → clock-B8vmwkHj.js} +1 -1
  34. package/lib/public/assets/{copy-C6zerX3I.js → copy-BAqnFI-F.js} +1 -1
  35. package/lib/public/assets/{device-explorer-B1pI6b_M.js → device-explorer-CYq_4F_N.js} +3 -3
  36. package/lib/public/assets/device-explorer-D2AqrrLN.css +1 -0
  37. package/lib/public/assets/{download-x9WP7mcL.js → download-B6UEAp_1.js} +1 -1
  38. package/lib/public/assets/{forgot-password-B0TEH4N-.js → forgot-password-CLqNvmrN.js} +1 -1
  39. package/lib/public/assets/{funnel-Cb40MfFN.js → funnel-Da90BBbT.js} +1 -1
  40. package/lib/public/assets/{index-BR1wTIPC.js → index-BuIIcmGp.js} +2 -2
  41. package/lib/public/assets/{index-BvH_ekRy.js → index-MX7IzqyW.js} +1 -1
  42. package/lib/public/assets/{input-DejcCiTI.js → input-Bq9aY-We.js} +1 -1
  43. package/lib/public/assets/{keycodes-Mavx5iGg.js → keycodes-CIhXIJyP.js} +1 -1
  44. package/lib/public/assets/{layers-BKqYLiHE.js → layers-bXjpswQI.js} +1 -1
  45. package/lib/public/assets/{lock-LFsFhc17.js → lock-pJ72y5Fy.js} +1 -1
  46. package/lib/public/assets/{login-x2pkzt-t.js → login-UOcHALV2.js} +1 -1
  47. package/lib/public/assets/{maintenance-settings-CYafcoV-.js → maintenance-settings-XnJgp5yb.js} +1 -1
  48. package/lib/public/assets/{monitor-_7_uJ8d9.js → monitor-QkAhI1Jx.js} +1 -1
  49. package/lib/public/assets/{mouse-pointer-2-BfLuURKN.js → mouse-pointer-2-B42Da0m4.js} +1 -1
  50. package/lib/public/assets/{network-Z_zBhpsD.js → network-CKVqcyp6.js} +1 -1
  51. package/lib/public/assets/{overview-BY5uv08S.js → overview-BORGkN3N.js} +1 -1
  52. package/lib/public/assets/{page-header-CP2UkykG.js → page-header-CKWFOjOI.js} +1 -1
  53. package/lib/public/assets/{plus-BTD0dnQ0.js → plus-BpFKVdC8.js} +1 -1
  54. package/lib/public/assets/{profile-page-BhJZuCS7.js → profile-page-CcbKBicY.js} +1 -1
  55. package/lib/public/assets/{reset-password-CMN0SO81.js → reset-password-BA0W9bxl.js} +1 -1
  56. package/lib/public/assets/{runbook-page-CBpPX9_5.js → runbook-page-Cp7ZBq3s.js} +1 -1
  57. package/lib/public/assets/{select-Uc9E5pye.js → select-DIZWIoVR.js} +1 -1
  58. package/lib/public/assets/{selector-detail-page-BSyBnRuU.js → selector-detail-page-C_4YuvAp.js} +1 -1
  59. package/lib/public/assets/{selector-health-DzqxNkzm.js → selector-health-CXjD_Xth.js} +1 -1
  60. package/lib/public/assets/{selector-health-page-CyPLjoD1.js → selector-health-page-CTqIHeoG.js} +1 -1
  61. package/lib/public/assets/{session-detail-page-BZQZN1P-.js → session-detail-page-B0KYkk8z.js} +1 -1
  62. package/lib/public/assets/{settings-CkSWiZq8.js → settings-BcRBKUXd.js} +1 -1
  63. package/lib/public/assets/{status-pill-outline-BFYni2qo.js → status-pill-outline-Dh2PXSf_.js} +1 -1
  64. package/lib/public/assets/{tablet-BSd-xSZK.js → tablet-_sXqZztt.js} +1 -1
  65. package/lib/public/assets/{teams-PpoEQz31.js → teams-DGHT8MKm.js} +1 -1
  66. package/lib/public/assets/{trash-2-BorJv5_u.js → trash-2-BV88cV3Z.js} +1 -1
  67. package/lib/public/assets/{upload-C3i7sAZl.js → upload-CIDH9wkj.js} +1 -1
  68. package/lib/public/assets/{use-password-reset-mode-CeZQ3EsH.js → use-password-reset-mode-DYGuSNwl.js} +1 -1
  69. package/lib/public/assets/{users-CEJ3fC78.js → users-7yviqflc.js} +1 -1
  70. package/lib/public/assets/{users-Drd7ifJx.js → users-BUE7MoTq.js} +1 -1
  71. package/lib/public/assets/{video-off-CNzn6bPE.js → video-off-BTRxtlGo.js} +1 -1
  72. package/lib/public/assets/{webhook-settings-BFqX6QXV.js → webhook-settings-DMKD9Pgm.js} +1 -1
  73. package/lib/public/assets/{zap-4yN8Jdfw.js → zap-CWE94qcK.js} +1 -1
  74. package/lib/public/index.html +1 -1
  75. package/lib/src/InternalHttpClient.js +5 -1
  76. package/lib/src/XenonCapabilityManager.js +15 -1
  77. package/lib/src/app/appiumBasePath.js +32 -0
  78. package/lib/src/app/index.js +12 -0
  79. package/lib/src/app/insertBeforeRoutes.js +24 -4
  80. package/lib/src/app/registerCommandAuth.js +152 -33
  81. package/lib/src/app/routers/control.js +57 -13
  82. package/lib/src/app/routers/grid.js +28 -8
  83. package/lib/src/app/ws/h264StreamWs.js +13 -8
  84. package/lib/src/app/ws/logcatWs.js +12 -7
  85. package/lib/src/app/ws/upgradeRouter.js +215 -0
  86. package/lib/src/dashboard/event-manager.js +7 -6
  87. package/lib/src/data-service/device-service.js +152 -49
  88. package/lib/src/data-service/device-store.js +81 -10
  89. package/lib/src/data-service/deviceClaims.js +98 -0
  90. package/lib/src/data-service/deviceFieldOwners.js +32 -10
  91. package/lib/src/data-service/prisma-store.js +94 -31
  92. package/lib/src/device-managers/AndroidDeviceManager.js +13 -15
  93. package/lib/src/device-managers/HealthMonitorService.js +17 -2
  94. package/lib/src/device-managers/IOSDeviceManager.js +5 -1
  95. package/lib/src/device-managers/NodeDevices.js +7 -2
  96. package/lib/src/device-managers/android/AndroidH264StreamService.js +3 -3
  97. package/lib/src/device-managers/android/AndroidStreamService.js +6 -5
  98. package/lib/src/device-managers/android/previewHold.js +2 -2
  99. package/lib/src/device-managers/ios/IOSDiscoveryService.js +19 -8
  100. package/lib/src/device-managers/ios/IOSStreamService.js +158 -38
  101. package/lib/src/device-managers/localDeviceHosts.js +86 -0
  102. package/lib/src/device-managers/ownDeviceRow.js +38 -0
  103. package/lib/src/device-utils.js +78 -24
  104. package/lib/src/gateway/createHandoff.js +73 -0
  105. package/lib/src/gateway/defaultGateway.js +80 -0
  106. package/lib/src/gateway/forwardToNode.js +147 -0
  107. package/lib/src/gateway/hubSessionToken.js +356 -0
  108. package/lib/src/gateway/internalCall.js +90 -0
  109. package/lib/src/gateway/nodeSessionStatus.js +140 -0
  110. package/lib/src/gateway/nodeWebDriverUrl.js +139 -0
  111. package/lib/src/gateway/sessionCreate.js +136 -0
  112. package/lib/src/gateway/sessionGateway.js +201 -0
  113. package/lib/src/gateway/sessionLocator.js +100 -0
  114. package/lib/src/generated/client/edge.js +7 -4
  115. package/lib/src/generated/client/index-browser.js +4 -1
  116. package/lib/src/generated/client/index.d.ts +112 -1
  117. package/lib/src/generated/client/index.js +7 -4
  118. package/lib/src/generated/client/package.json +1 -1
  119. package/lib/src/generated/client/schema.prisma +9 -0
  120. package/lib/src/generated/client/wasm.js +4 -1
  121. package/lib/src/helpers/index.js +5 -1
  122. package/lib/src/logger.js +5 -0
  123. package/lib/src/middleware/authMiddleware.js +80 -0
  124. package/lib/src/middleware/commandAuth.js +31 -17
  125. package/lib/src/middleware/csrfMiddleware.js +3 -2
  126. package/lib/src/middleware/sessionListingFilter.js +153 -0
  127. package/lib/src/middleware/sessionUpgradeGuard.js +203 -0
  128. package/lib/src/plugin.js +27 -1
  129. package/lib/src/prisma.js +2 -1
  130. package/lib/src/services/DeviceReconciler.js +5 -2
  131. package/lib/src/services/OrphanSweeper.js +12 -19
  132. package/lib/src/services/PortAllocator.js +75 -16
  133. package/lib/src/services/ServerManager.js +50 -30
  134. package/lib/src/services/SessionLifecycleService.js +335 -76
  135. package/lib/src/services/ShutdownCoordinator.js +7 -1
  136. package/lib/src/services/SocketServer.js +11 -2
  137. package/lib/src/services/device-access/LiveSessionOwners.js +53 -0
  138. package/lib/src/services/device-access/SessionOwnerResolver.js +74 -0
  139. package/lib/src/services/lease/LeaseOrphanSweeper.js +32 -6
  140. package/lib/src/services/lease/LeaseService.js +19 -5
  141. package/lib/src/services/session/nodeCreateCaps.js +55 -0
  142. package/lib/src/services/session/sessionCredentials.js +7 -59
  143. package/lib/src/sessions/CloudSession.js +12 -0
  144. package/lib/src/sessions/LocalSession.js +52 -26
  145. package/lib/src/sessions/RemoteSession.js +116 -9
  146. package/lib/src/sessions/SessionManager.js +49 -6
  147. package/lib/src/sessions/appiumUmbrella.js +32 -0
  148. package/lib/test/helpers/appium-umbrella.js +84 -0
  149. package/lib/test/helpers/fake-device-table.js +97 -0
  150. package/lib/test/helpers/loki-stores.js +41 -0
  151. package/lib/test/helpers/loopbackRequest.js +77 -0
  152. package/lib/test/helpers/scratch-database.js +100 -0
  153. package/lib/test/helpers/scratch-port-leases.js +119 -0
  154. package/lib/test/helpers/test-container.js +35 -27
  155. package/lib/test/integration/androidDevices.spec.js +5 -4
  156. package/lib/test/integration/ios/01iOSSimulator.spec.js +6 -5
  157. package/lib/test/integration/ios/02iOSDevices.spec.js +3 -2
  158. package/lib/test/integration/prisma-device-store-add-devices.spec.js +14 -4
  159. package/lib/test/unit/AndroidDeviceManager.spec.js +8 -1
  160. package/lib/test/unit/DeviceModel.spec.js +9 -9
  161. package/lib/test/unit/HealingCascade.test.js +5 -1
  162. package/lib/test/unit/OrphanSweeper.test.js +17 -13
  163. package/lib/test/unit/SessionHeartbeatWrite.test.js +6 -7
  164. package/lib/test/unit/StreamTicketService.spec.js +4 -1
  165. package/lib/test/unit/TokenEndpoint.spec.js +17 -3
  166. package/lib/test/unit/aggregate-hotspots-session-scope.spec.js +5 -5
  167. package/lib/test/unit/android-h264-service.spec.js +6 -0
  168. package/lib/test/unit/android-stream-stall-eof.spec.js +10 -0
  169. package/lib/test/unit/apps-team-visibility.spec.js +16 -16
  170. package/lib/test/unit/auth-me-auth-disabled.test.js +7 -4
  171. package/lib/test/unit/authMiddleware.appTicket.spec.js +5 -5
  172. package/lib/test/unit/authMiddleware.bearer.spec.js +4 -1
  173. package/lib/test/unit/authMiddleware.mcpAudience.spec.js +4 -1
  174. package/lib/test/unit/authMiddleware.streamTicket.spec.js +19 -7
  175. package/lib/test/unit/authMiddleware.test.js +3 -0
  176. package/lib/test/unit/bug-report/route.spec.js +9 -9
  177. package/lib/test/unit/cliargs-admin-only.spec.js +3 -3
  178. package/lib/test/unit/clipboard-route.spec.js +18 -8
  179. package/lib/test/unit/command-auth-appium-server.spec.js +187 -32
  180. package/lib/test/unit/command-auth.spec.js +31 -31
  181. package/lib/test/unit/control-device-lookup.spec.js +9 -9
  182. package/lib/test/unit/control-forward-decision.spec.js +115 -0
  183. package/lib/test/unit/control-lookup-failure.spec.js +3 -3
  184. package/lib/test/unit/create-error-message.spec.js +43 -0
  185. package/lib/test/unit/create-handoff.spec.js +86 -0
  186. package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
  187. package/lib/test/unit/device-access-guard.spec.js +28 -28
  188. package/lib/test/unit/device-service.spec.js +4 -0
  189. package/lib/test/unit/device-store-clear-at-boot.spec.js +48 -0
  190. package/lib/test/unit/device-sync-lost-update.spec.js +17 -89
  191. package/lib/test/unit/device-team-guard.spec.js +20 -20
  192. package/lib/test/unit/device-utils.spec.js +8 -0
  193. package/lib/test/unit/extract-team-cap.spec.js +65 -0
  194. package/lib/test/unit/grid-admin-only.spec.js +3 -3
  195. package/lib/test/unit/healing-tier-gate.spec.js +5 -1
  196. package/lib/test/unit/health-monitor-own-phones.spec.js +102 -0
  197. package/lib/test/unit/health-monitor-stream-lookup.spec.js +11 -1
  198. package/lib/test/unit/helpers.spec.js +27 -0
  199. package/lib/test/unit/hub-boot-sweep-recovery.spec.js +192 -0
  200. package/lib/test/unit/hub-create-once.spec.js +190 -0
  201. package/lib/test/unit/hub-create-session-umbrella.spec.js +160 -0
  202. package/lib/test/unit/hub-gateway-create-appium-server.spec.js +591 -0
  203. package/lib/test/unit/hub-node-control-forward.spec.js +291 -0
  204. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +594 -0
  205. package/lib/test/unit/hub-node-race.spec.js +401 -0
  206. package/lib/test/unit/hub-node-same-host.spec.js +356 -0
  207. package/lib/test/unit/hub-register-removal.spec.js +165 -0
  208. package/lib/test/unit/hub-remote-session-heartbeat.spec.js +277 -0
  209. package/lib/test/unit/hub-routed-session-recovery.spec.js +151 -0
  210. package/lib/test/unit/hub-session-token.spec.js +265 -0
  211. package/lib/test/unit/insert-before-routes.spec.js +5 -5
  212. package/lib/test/unit/install-repository-app-team.spec.js +2 -2
  213. package/lib/test/unit/interceptor/MitmProxyHost.spec.js +23 -10
  214. package/lib/test/unit/internal-call.spec.js +181 -0
  215. package/lib/test/unit/ios-discovery-same-host.spec.js +193 -0
  216. package/lib/test/unit/ios-stream-appium-tunnel.spec.js +317 -0
  217. package/lib/test/unit/ios-stream-port-leases.spec.js +299 -0
  218. package/lib/test/unit/ios-wda-status-identity.spec.js +84 -0
  219. package/lib/test/unit/is-xenon-running.spec.js +69 -0
  220. package/lib/test/unit/lease/LeaseOrphanSweeper.spec.js +2 -2
  221. package/lib/test/unit/lease/LeaseService.spec.js +28 -1
  222. package/lib/test/unit/lease/lease-reap-held-phone.spec.js +154 -0
  223. package/lib/test/unit/lease/lease-sweeper-expiry.spec.js +92 -0
  224. package/lib/test/unit/lease/lease-team-visibility.spec.js +2 -2
  225. package/lib/test/unit/lease/lease-token-log-filter.spec.js +242 -64
  226. package/lib/test/unit/lease/lease-token-oracle.spec.js +20 -7
  227. package/lib/test/unit/lease/session-lease-proof.spec.js +8 -3
  228. package/lib/test/unit/logcat-ws.spec.js +4 -1
  229. package/lib/test/unit/loopback-request.spec.js +81 -0
  230. package/lib/test/unit/node-create-caps.spec.js +54 -0
  231. package/lib/test/unit/node-gateway-create-appium-server.spec.js +354 -0
  232. package/lib/test/unit/node-hub-session-owner.spec.js +223 -0
  233. package/lib/test/unit/omni-vision.spec.js +7 -3
  234. package/lib/test/unit/plugin.spec.js +45 -4
  235. package/lib/test/unit/port-allocator-os-probe.spec.js +151 -0
  236. package/lib/test/unit/profile-router.test.js +11 -11
  237. package/lib/test/unit/queue-requester.spec.js +5 -0
  238. package/lib/test/unit/queue-team-scope-routes.spec.js +17 -16
  239. package/lib/test/unit/queue-visibility.spec.js +1 -1
  240. package/lib/test/unit/recording-downloads-visibility.spec.js +2 -2
  241. package/lib/test/unit/recording-instrumentation.spec.js +5 -0
  242. package/lib/test/unit/recording-source-ended.spec.js +5 -0
  243. package/lib/test/unit/recording-store.spec.js +9 -3
  244. package/lib/test/unit/recordings-library-routes.spec.js +51 -51
  245. package/lib/test/unit/recordings-write-visibility.spec.js +2 -2
  246. package/lib/test/unit/remote-session-create.spec.js +130 -0
  247. package/lib/test/unit/reservation-team-visibility.spec.js +5 -5
  248. package/lib/test/unit/reset-link.test.js +14 -14
  249. package/lib/test/unit/session-app-resolution.spec.js +14 -10
  250. package/lib/test/unit/session-assets.spec.js +9 -9
  251. package/lib/test/unit/session-attribution-wiring.spec.js +29 -2
  252. package/lib/test/unit/session-create-layer.spec.js +255 -0
  253. package/lib/test/unit/session-credential-strip.spec.js +25 -9
  254. package/lib/test/unit/session-credentials.spec.js +0 -47
  255. package/lib/test/unit/session-listing-auth.spec.js +345 -0
  256. package/lib/test/unit/session-locator.spec.js +250 -0
  257. package/lib/test/unit/session-node-calls.spec.js +174 -0
  258. package/lib/test/unit/session-owner-resolver.spec.js +107 -0
  259. package/lib/test/unit/session-recovery-routing.spec.js +118 -0
  260. package/lib/test/unit/session-upgrade-guard.spec.js +571 -0
  261. package/lib/test/unit/shutdown-drain-remote.spec.js +58 -0
  262. package/lib/test/unit/socketServer.bearer.spec.js +3 -1
  263. package/lib/test/unit/stream-leave-route.spec.js +15 -7
  264. package/lib/test/unit/stream-own-device-row.spec.js +164 -0
  265. package/lib/test/unit/stream-stop-ownership.spec.js +13 -5
  266. package/lib/test/unit/stream-stop-recording.spec.js +13 -5
  267. package/lib/test/unit/stream-ticket-identity.spec.js +15 -5
  268. package/lib/test/unit/upgrade-router.spec.js +446 -0
  269. package/lib/test/unit/users-router.test.js +11 -8
  270. package/lib/test/unit/verify-credential.spec.js +4 -1
  271. package/lib/test/unit/webdriver-info-route.spec.js +64 -0
  272. package/lib/test/unit/ws-upgrade-routing-appium-server.spec.js +500 -0
  273. package/lib/tsconfig.tsbuildinfo +1 -1
  274. package/package.json +1 -2
  275. package/prisma/migrations/20260929120000_device_claims/migration.sql +8 -0
  276. package/prisma/schema.prisma +9 -0
  277. package/lib/public/assets/BugReportButton-DKYsOdGV.js +0 -10
  278. package/lib/public/assets/device-explorer-DV8rWRxL.css +0 -1
  279. package/lib/src/proxy/wd-command-proxy.js +0 -221
@@ -0,0 +1,401 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ require("reflect-metadata");
49
+ const chai_1 = require("chai");
50
+ const sinon_1 = __importDefault(require("sinon"));
51
+ const express_1 = __importDefault(require("express"));
52
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
53
+ const typedi_1 = require("typedi");
54
+ const prisma_1 = require("../../src/prisma");
55
+ const grid_1 = __importDefault(require("../../src/app/routers/grid"));
56
+ const prisma_store_1 = require("../../src/data-service/prisma-store");
57
+ const device_store_1 = require("../../src/data-service/device-store");
58
+ const device_utils_1 = require("../../src/device-utils");
59
+ const localDeviceHosts_1 = require("../../src/device-managers/localDeviceHosts");
60
+ const SessionLifecycleService_1 = require("../../src/services/SessionLifecycleService");
61
+ const device_managers_1 = require("../../src/device-managers");
62
+ const PluginContext_1 = require("../../src/PluginContext");
63
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
64
+ const SocketServer_1 = require("../../src/services/SocketServer");
65
+ const NotificationService_1 = require("../../src/services/NotificationService");
66
+ const event_manager_1 = require("../../src/dashboard/event-manager");
67
+ const helpers = __importStar(require("../../src/helpers"));
68
+ const container_registration_1 = require("../helpers/container-registration");
69
+ const scratch_database_1 = require("../helpers/scratch-database");
70
+ /**
71
+ * The hub/node busy race, on a real SQLite database with the migrations
72
+ * applied. A hub keeps its nodes' phones in its own table, from each node's
73
+ * report (`POST /xenon/api/register`, every 30 s by default). The report used
74
+ * to be written whole: a node that had not yet been handed the hub's session
75
+ * said `busy: false` and freed the phone for a second client, and its copy of
76
+ * the team, tags, reservation and block replaced the hub's.
77
+ */
78
+ const IP = '192.168.0.104';
79
+ const HUB = `http://${IP}:4724`;
80
+ const NODE = `http://${IP}:4725`;
81
+ const HUB_ID = 'hub-process';
82
+ const NODE_ID = 'node-process';
83
+ const T0 = Date.UTC(2026, 8, 29, 12, 0, 0);
84
+ const nodePhone = (udid, over = {}) => (Object.assign({ udid, host: NODE, nodeId: NODE_ID, name: 'Galaxy S9+', platform: 'android', deviceType: 'real', realDevice: true, state: 'device', sdk: '10', offline: false, userBlocked: false }, over));
85
+ const caps = (udid) => ({
86
+ alwaysMatch: { platformName: 'Android', 'appium:udid': udid },
87
+ firstMatch: [{}],
88
+ });
89
+ describe('The hub/node busy race', function () {
90
+ this.timeout(60000);
91
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
92
+ let restoreContainer;
93
+ let savedStore;
94
+ let context;
95
+ let savedContext;
96
+ let clock;
97
+ const pluginArgs = Object.assign({}, IPluginArgs_1.DefaultPluginArgs);
98
+ const app = () => {
99
+ const a = (0, express_1.default)();
100
+ a.use(express_1.default.json());
101
+ a.use((req, _res, next) => {
102
+ req.auth = {
103
+ kind: 'api-key',
104
+ userId: 'node-user',
105
+ role: 'ADMIN',
106
+ scopes: 'devices',
107
+ rateLimit: 0,
108
+ };
109
+ next();
110
+ });
111
+ grid_1.default.register(a, pluginArgs);
112
+ return a;
113
+ };
114
+ /** The node's periodic report: its whole row for each phone. */
115
+ const report = (...phones) => __awaiter(this, void 0, void 0, function* () {
116
+ const res = yield (0, loopbackRequest_1.default)(app()).post('/register').query({ type: 'add' }).send(phones);
117
+ (0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
118
+ });
119
+ const row = (udid_1, ...args_1) => __awaiter(this, [udid_1, ...args_1], void 0, function* (udid, host = NODE) { return (yield scratch.db.device.findFirst({ where: { udid, host } })); });
120
+ /** The hub allocates a phone for a create, as prepareSession does. */
121
+ const allocate = (udid) => (0, device_utils_1.allocateDeviceForSession)(caps(udid), 2000, 50, pluginArgs);
122
+ /** The create succeeded: the session is recorded on its phone. */
123
+ const finalize = (device, sessionId) => __awaiter(this, void 0, void 0, function* () {
124
+ const svc = new SessionLifecycleService_1.SessionLifecycleService();
125
+ sinon_1.default.stub(svc, 'createSessionInstance').returns({});
126
+ sinon_1.default.stub(svc, 'applyPostSessionLogic').resolves();
127
+ yield svc.finalizeSession({ protocol: 'W3C', value: [sessionId, { platformName: 'Android' }, 'W3C'] }, device, caps(device.udid), {}, false);
128
+ });
129
+ const deleteSession = (sessionId) => new SessionLifecycleService_1.SessionLifecycleService().deleteSession(() => __awaiter(this, void 0, void 0, function* () { return null; }), sessionId);
130
+ /** Let the sweepers' fire-and-forget releases land before looking. */
131
+ const settle = () => new Promise((resolve) => setTimeout(resolve, 150));
132
+ beforeEach(() => __awaiter(this, void 0, void 0, function* () {
133
+ restoreContainer = (0, container_registration_1.saveRegistrations)(device_managers_1.XenonManager, SocketServer_1.SocketServer, NotificationService_1.NotificationService, 'LocalStorage');
134
+ typedi_1.Container.set({
135
+ id: device_managers_1.XenonManager,
136
+ value: {
137
+ getMaxSessionCount: () => undefined,
138
+ deviceInstances: () => __awaiter(this, void 0, void 0, function* () { return []; }),
139
+ getDevices: () => __awaiter(this, void 0, void 0, function* () { return []; }),
140
+ },
141
+ });
142
+ typedi_1.Container.set({
143
+ id: SocketServer_1.SocketServer,
144
+ value: {
145
+ emitToDashboard: () => undefined,
146
+ emitToDashboardForDevices: () => __awaiter(this, void 0, void 0, function* () { return undefined; }),
147
+ hasScopedDashboard: () => false,
148
+ },
149
+ });
150
+ typedi_1.Container.set({ id: NotificationService_1.NotificationService, value: { dispatchEvent: () => __awaiter(this, void 0, void 0, function* () { return undefined; }) } });
151
+ typedi_1.Container.set({
152
+ id: 'LocalStorage',
153
+ value: { getItem: () => 0, setItem: () => __awaiter(this, void 0, void 0, function* () { return undefined; }) },
154
+ });
155
+ sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'onSessionStopped').resolves();
156
+ context = typedi_1.Container.get(PluginContext_1.PluginContext);
157
+ savedContext = { nodeId: context.nodeId, pluginArgs: context.pluginArgs };
158
+ context.nodeId = HUB_ID;
159
+ context.pluginArgs = pluginArgs;
160
+ savedStore = device_store_1.DeviceStoreFactory._deviceStore;
161
+ device_store_1.DeviceStoreFactory._deviceStore = new prisma_store_1.PrismaDeviceStore();
162
+ yield scratch.db.device.deleteMany({});
163
+ yield scratch.db.team.deleteMany({});
164
+ }));
165
+ afterEach(() => {
166
+ clock === null || clock === void 0 ? void 0 : clock.restore();
167
+ clock = undefined;
168
+ device_store_1.DeviceStoreFactory._deviceStore = savedStore;
169
+ Object.assign(context, savedContext);
170
+ sinon_1.default.restore();
171
+ restoreContainer();
172
+ });
173
+ describe("a node's report while the hub holds a claim", () => {
174
+ it('leaves the phone busy while the session is created, and while it runs', () => __awaiter(this, void 0, void 0, function* () {
175
+ yield scratch.db.device.create({ data: nodePhone('s9') });
176
+ const device = yield allocate('s9');
177
+ // The node hasn't been handed the create yet: free, as far as it knows.
178
+ yield report(nodePhone('s9', { busy: false }));
179
+ (0, chai_1.expect)(yield row('s9')).to.include({ busy: true });
180
+ yield finalize(device, 'hub-session-1');
181
+ yield report(nodePhone('s9', { busy: false, session_id: null }));
182
+ (0, chai_1.expect)(yield row('s9')).to.include({ busy: true, session_id: 'hub-session-1' });
183
+ // A second client can't be given the phone.
184
+ let second;
185
+ try {
186
+ second = yield (0, device_utils_1.allocateDeviceForSession)(caps('s9'), 200, 50, pluginArgs);
187
+ }
188
+ catch (err) {
189
+ second = err;
190
+ }
191
+ (0, chai_1.expect)(second).to.be.an('error');
192
+ }));
193
+ it('keeps the phone busy after the hub ends its claim while the node still reports it busy', () => __awaiter(this, void 0, void 0, function* () {
194
+ yield scratch.db.device.create({ data: nodePhone('s9') });
195
+ const device = yield allocate('s9');
196
+ yield finalize(device, 'hub-session-1');
197
+ yield report(nodePhone('s9', { busy: true, session_id: 'hub-session-1' }));
198
+ yield deleteSession('hub-session-1');
199
+ (0, chai_1.expect)(yield row('s9')).to.include({ busy: true, nodeBusy: true, session_id: null });
200
+ yield report(nodePhone('s9', { busy: false, session_id: null }));
201
+ (0, chai_1.expect)(yield row('s9')).to.include({ busy: false, nodeBusy: false });
202
+ }));
203
+ it('keeps a hold the hub took (a preview, a recording) when the node reports the phone free', () => __awaiter(this, void 0, void 0, function* () {
204
+ yield scratch.db.device.create({
205
+ data: nodePhone('s9', { busy: true, session_id: 'manual_u1_s9' }),
206
+ });
207
+ yield report(nodePhone('s9', { busy: false, session_id: null }));
208
+ (0, chai_1.expect)(yield row('s9')).to.include({
209
+ busy: true,
210
+ nodeBusy: false,
211
+ session_id: 'manual_u1_s9',
212
+ });
213
+ }));
214
+ it("counts a node's own busy with no claim, and frees it when the node does", () => __awaiter(this, void 0, void 0, function* () {
215
+ yield scratch.db.device.create({ data: nodePhone('s9') });
216
+ yield report(nodePhone('s9', { busy: true, session_id: 'node-local' }));
217
+ (0, chai_1.expect)(yield row('s9')).to.include({ busy: true, nodeBusy: true });
218
+ yield report(nodePhone('s9', { busy: false, session_id: null }));
219
+ (0, chai_1.expect)(yield row('s9')).to.include({ busy: false, nodeBusy: false });
220
+ }));
221
+ });
222
+ describe("a node's report and the settings the hub owns", () => {
223
+ it('never overwrites the team, tags, reservation or block', () => __awaiter(this, void 0, void 0, function* () {
224
+ const team = yield scratch.db.team.create({ data: { name: 'Payments' } });
225
+ const reservedUntil = Date.now() + 3600000;
226
+ yield scratch.db.device.create({
227
+ data: Object.assign(Object.assign({}, nodePhone('s9')), { teamId: team.id, tags: JSON.stringify(['smoke']), userBlocked: true, reservedBy: 'alice', reservedUntil, reservationReason: 'release check' }),
228
+ });
229
+ // The node's copy of the row: none of the hub's settings.
230
+ yield report(nodePhone('s9', {
231
+ busy: false,
232
+ sdk: '11',
233
+ teamId: null,
234
+ tags: [],
235
+ userBlocked: false,
236
+ reservedBy: null,
237
+ reservedUntil: null,
238
+ reservationReason: null,
239
+ }));
240
+ (0, chai_1.expect)(yield row('s9')).to.include({
241
+ sdk: '11',
242
+ teamId: team.id,
243
+ tags: JSON.stringify(['smoke']),
244
+ userBlocked: true,
245
+ reservedBy: 'alice',
246
+ reservedUntil,
247
+ reservationReason: 'release check',
248
+ });
249
+ }));
250
+ });
251
+ describe('a release for an ended session', () => {
252
+ it("doesn't free a phone another session has claimed since", () => __awaiter(this, void 0, void 0, function* () {
253
+ yield scratch.db.device.create({ data: nodePhone('s9') });
254
+ const svc = new SessionLifecycleService_1.SessionLifecycleService();
255
+ // Create A takes the phone, and stalls.
256
+ const a = yield allocate('s9');
257
+ // Its claim is given up (a timeout), and create B takes the phone.
258
+ yield scratch.db.device.updateMany({
259
+ where: { udid: 's9' },
260
+ data: { busy: false, lastCmdExecutedAt: null },
261
+ });
262
+ const b = yield allocate('s9');
263
+ yield finalize(b, 'session-b');
264
+ // Create A fails at last and gives its phone back.
265
+ yield svc.releaseAllocation({ device: a, pendingSessionId: 'pending-a' });
266
+ (0, chai_1.expect)(yield row('s9')).to.include({ busy: true, session_id: 'session-b' });
267
+ }));
268
+ it("doesn't free a same-machine hub's row for the same udid", () => __awaiter(this, void 0, void 0, function* () {
269
+ yield scratch.db.device.create({ data: nodePhone('s9') });
270
+ yield scratch.db.device.create({
271
+ data: Object.assign(Object.assign({}, nodePhone('s9')), { host: HUB, nodeId: HUB_ID }),
272
+ });
273
+ const svc = new SessionLifecycleService_1.SessionLifecycleService();
274
+ // Two creates for the udid: one gets each row.
275
+ const running = yield allocate('s9');
276
+ yield finalize(running, 'session-running');
277
+ const failing = yield allocate('s9');
278
+ (0, chai_1.expect)([running.host, failing.host]).to.have.members([HUB, NODE]);
279
+ // The second create fails and gives its phone back.
280
+ yield svc.releaseAllocation({ device: failing, pendingSessionId: 'pending' });
281
+ (0, chai_1.expect)(yield row('s9', failing.host)).to.include({ busy: false });
282
+ (0, chai_1.expect)(yield row('s9', running.host)).to.include({
283
+ busy: true,
284
+ session_id: 'session-running',
285
+ });
286
+ }));
287
+ });
288
+ describe('a claim still being created, and the idle sweeper', () => {
289
+ it('is not idle at the new-command timeout, and is freed at its own timeout', () => __awaiter(this, void 0, void 0, function* () {
290
+ clock = sinon_1.default.useFakeTimers({ now: T0, toFake: ['Date'] });
291
+ yield scratch.db.device.create({ data: nodePhone('s9') });
292
+ yield allocate('s9');
293
+ // A first driver install: two minutes, past the 60 s new-command timeout.
294
+ clock.setSystemTime(T0 + 120000);
295
+ yield (0, device_utils_1.releaseBlockedDevices)(60);
296
+ yield settle();
297
+ (0, chai_1.expect)(yield row('s9')).to.include({ busy: true });
298
+ // A create that never finished.
299
+ clock.setSystemTime(T0 + 11 * 60000);
300
+ yield (0, device_utils_1.releaseBlockedDevices)(60);
301
+ yield settle();
302
+ (0, chai_1.expect)(yield row('s9')).to.include({ busy: false });
303
+ }));
304
+ it('frees a created session that went idle, as before', () => __awaiter(this, void 0, void 0, function* () {
305
+ clock = sinon_1.default.useFakeTimers({ now: T0, toFake: ['Date'] });
306
+ yield scratch.db.device.create({ data: nodePhone('s9') });
307
+ yield finalize(yield allocate('s9'), 'idle-session');
308
+ clock.setSystemTime(T0 + 120000);
309
+ yield (0, device_utils_1.releaseBlockedDevices)(60);
310
+ yield settle();
311
+ (0, chai_1.expect)(yield row('s9')).to.include({ busy: false, session_id: null });
312
+ }));
313
+ });
314
+ describe('a release racing a new claim', () => {
315
+ it('a release that read the phone before its next claim leaves that claim alone', () => __awaiter(this, void 0, void 0, function* () {
316
+ clock = sinon_1.default.useFakeTimers({ now: T0, toFake: ['Date'] });
317
+ yield scratch.db.device.create({ data: nodePhone('s9') });
318
+ yield finalize(yield allocate('s9'), 'session-a');
319
+ clock.setSystemTime(T0 + 120000);
320
+ // The idle sweeper reads the phone with session A on it. Before it
321
+ // writes, A is deleted and session B claims the phone.
322
+ const findMany = prisma_1.prisma.device.findMany;
323
+ let interleaved = false;
324
+ findMany.callsFake((args) => __awaiter(this, void 0, void 0, function* () {
325
+ const rows = yield scratch.db.device.findMany(args);
326
+ if (!interleaved && !(args === null || args === void 0 ? void 0 : args.where)) {
327
+ interleaved = true;
328
+ yield deleteSession('session-a');
329
+ yield finalize(yield allocate('s9'), 'session-b');
330
+ }
331
+ return rows;
332
+ }));
333
+ yield (0, device_utils_1.releaseBlockedDevices)(60);
334
+ yield settle();
335
+ (0, chai_1.expect)(interleaved).to.equal(true);
336
+ (0, chai_1.expect)(yield row('s9')).to.include({ busy: true, session_id: 'session-b' });
337
+ }));
338
+ it('two releases of one session leave the next claim alone', () => __awaiter(this, void 0, void 0, function* () {
339
+ yield scratch.db.device.create({ data: nodePhone('s9') });
340
+ yield finalize(yield allocate('s9'), 'session-a');
341
+ yield Promise.all([deleteSession('session-a'), deleteSession('session-a')]);
342
+ yield finalize(yield allocate('s9'), 'session-b');
343
+ yield deleteSession('session-a');
344
+ (0, chai_1.expect)(yield row('s9')).to.include({ busy: true, session_id: 'session-b' });
345
+ }));
346
+ });
347
+ describe('stale cleanup of node phones', () => {
348
+ // The hub also files iPhones under a bare remoteMachineProxyIP, and so
349
+ // does the node: the node's iPhone has a host the hub calls its own.
350
+ const hubHosts = (0, localDeviceHosts_1.localDeviceHosts)({ bindHostOrIp: IP, remoteMachineProxyIP: '203.0.113.7' }, 4724);
351
+ const nodeIphone = () => nodePhone('iphone', { host: '203.0.113.7', platform: 'ios', deviceType: 'real' });
352
+ it("the hub's local sync never prunes a phone carrying the node's id", () => __awaiter(this, void 0, void 0, function* () {
353
+ yield scratch.db.device.create({ data: nodeIphone() });
354
+ yield scratch.db.device.create({
355
+ data: Object.assign(Object.assign({}, nodePhone('gone')), { host: HUB, nodeId: HUB_ID }),
356
+ });
357
+ yield (0, device_utils_1.updateDeviceList)(hubHosts);
358
+ (0, chai_1.expect)(yield row('iphone', '203.0.113.7')).to.include({ nodeId: NODE_ID });
359
+ (0, chai_1.expect)(yield row('gone', HUB)).to.equal(null);
360
+ }));
361
+ it("drops the node's phones with the node, keyed on its id", () => __awaiter(this, void 0, void 0, function* () {
362
+ yield scratch.db.device.create({ data: nodeIphone() });
363
+ yield scratch.db.device.create({ data: nodePhone('s9') });
364
+ yield scratch.db.device.create({
365
+ data: Object.assign(Object.assign({}, nodePhone('hub-phone')), { host: HUB, nodeId: HUB_ID }),
366
+ });
367
+ const alive = sinon_1.default.stub(helpers, 'isXenonRunning').callsFake((host) => __awaiter(this, void 0, void 0, function* () { return host === NODE; }));
368
+ yield (0, device_utils_1.removeStaleDevices)(hubHosts);
369
+ (0, chai_1.expect)(yield row('iphone', '203.0.113.7'), 'kept while its node answers').to.not.equal(null);
370
+ (0, chai_1.expect)(yield row('s9')).to.not.equal(null);
371
+ alive.callsFake(() => __awaiter(this, void 0, void 0, function* () { return false; }));
372
+ yield (0, device_utils_1.removeStaleDevices)(hubHosts);
373
+ (0, chai_1.expect)(yield row('iphone', '203.0.113.7'), 'dropped with its node').to.equal(null);
374
+ (0, chai_1.expect)(yield row('s9')).to.equal(null);
375
+ (0, chai_1.expect)(yield row('hub-phone', HUB), "the hub's own").to.not.equal(null);
376
+ (0, chai_1.expect)(alive.calledWith(HUB)).to.equal(false);
377
+ }));
378
+ });
379
+ describe('a standalone server', () => {
380
+ it('allocates, runs and frees its own phone as before', () => __awaiter(this, void 0, void 0, function* () {
381
+ yield scratch.db.device.create({
382
+ data: Object.assign(Object.assign({}, nodePhone('pixel')), { host: HUB, nodeId: HUB_ID }),
383
+ });
384
+ const device = yield allocate('pixel');
385
+ (0, chai_1.expect)(yield row('pixel', HUB)).to.include({ busy: true });
386
+ yield finalize(device, 'local-session');
387
+ (0, chai_1.expect)(yield row('pixel', HUB)).to.include({ busy: true, session_id: 'local-session' });
388
+ yield deleteSession('local-session');
389
+ (0, chai_1.expect)(yield row('pixel', HUB)).to.include({
390
+ busy: false,
391
+ session_id: null,
392
+ lastCmdExecutedAt: null,
393
+ sessionStartTime: 0,
394
+ });
395
+ (0, chai_1.expect)(yield row('pixel', HUB)).to.not.include({ nodeBusy: true });
396
+ // And it can be allocated again.
397
+ yield allocate('pixel');
398
+ (0, chai_1.expect)(yield row('pixel', HUB)).to.include({ busy: true });
399
+ }));
400
+ });
401
+ });