@xenon-device-management/xenon 2.0.0 → 2.2.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 (289) hide show
  1. package/README.md +2 -1
  2. package/lib/package.json +3 -4
  3. package/lib/public/assets/{AnnotationOverlay-BmTPCAds.js → AnnotationOverlay-XpewX5nn.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-B5eB7f8B.js → ApiKeyGate-BQDv3ZCC.js} +1 -1
  5. package/lib/public/assets/BugReportButton-C7OmS61B.js +10 -0
  6. package/lib/public/assets/{DeviceMosaicView-9DUoZnQi.js → DeviceMosaicView-wl3l2Zlt.js} +1 -1
  7. package/lib/public/assets/{EmptyState-lvyNiXev.js → EmptyState-fPxujqqE.js} +1 -1
  8. package/lib/public/assets/{FieldGroup-BduMPM9K.js → FieldGroup-5bCutWrs.js} +1 -1
  9. package/lib/public/assets/{FilterMenu-BSqX6I94.js → FilterMenu-owsBiDT1.js} +1 -1
  10. package/lib/public/assets/{Menu-Ba5cT7jZ.js → Menu-DFhqzPSt.js} +1 -1
  11. package/lib/public/assets/{Modal-CceoMY7D.js → Modal-Bw-a-E-e.js} +1 -1
  12. package/lib/public/assets/{RecordingPage-6T9dOidc.js → RecordingPage-WtByf5bC.js} +1 -1
  13. package/lib/public/assets/{RecordingsPage-qj6FdZXR.js → RecordingsPage-FpWWFNgT.js} +1 -1
  14. package/lib/public/assets/{SegmentedControl-CSRuZ8TR.js → SegmentedControl-Jzn9c7o8.js} +1 -1
  15. package/lib/public/assets/{SettingCard-Bp1kz4By.js → SettingCard-BR7nOhpT.js} +1 -1
  16. package/lib/public/assets/{Table-LV7zFyZg.js → Table-B_Va3V05.js} +1 -1
  17. package/lib/public/assets/{activity-D17TUloi.js → activity-DxRFWvkN.js} +1 -1
  18. package/lib/public/assets/{ai-settings-qvbhY89P.js → ai-settings-CQf-9_V6.js} +1 -1
  19. package/lib/public/assets/{api-keys-BfpvdG7G.js → api-keys-SoE1Kd7N.js} +1 -1
  20. package/lib/public/assets/{apps-Dbnzd0fW.js → apps-CgGXTlgn.js} +1 -1
  21. package/lib/public/assets/{arrow-left-DD1VjtC7.js → arrow-left-uQ6WIwiC.js} +1 -1
  22. package/lib/public/assets/{arrow-right-D0pnM3ma.js → arrow-right-C9RyfzXS.js} +1 -1
  23. package/lib/public/assets/{arrow-up-right-BIC8JOV6.js → arrow-up-right-DFHi_8yG.js} +1 -1
  24. package/lib/public/assets/{auth-shell-DWHPXSnI.js → auth-shell-Bii1t4qc.js} +2 -2
  25. package/lib/public/assets/{builds-page-ClCNBUv_.js → builds-page-Bnv8Z9hi.js} +1 -1
  26. package/lib/public/assets/{button-xtAhYR-f.js → button-_Y78wXiP.js} +1 -1
  27. package/lib/public/assets/{calendar-CupwJ7Bm.js → calendar-D4MqSOc2.js} +1 -1
  28. package/lib/public/assets/{check-CyuEoKkd.js → check-uGHCXe-L.js} +1 -1
  29. package/lib/public/assets/{chevron-left-DgX-F9xK.js → chevron-left-Dy1OqpzB.js} +1 -1
  30. package/lib/public/assets/{chevron-right-BKJgJnU9.js → chevron-right-BUsldEli.js} +1 -1
  31. package/lib/public/assets/{circle-check-CZdg8mMa.js → circle-check-D3Qq0tKp.js} +1 -1
  32. package/lib/public/assets/{circle-x-CZH6t9z-.js → circle-x-B4IOD1DE.js} +1 -1
  33. package/lib/public/assets/{clock-Cd-t7xpe.js → clock-BfYfzvSg.js} +1 -1
  34. package/lib/public/assets/{copy-C6zerX3I.js → copy-DZwYE5Va.js} +1 -1
  35. package/lib/public/assets/{device-explorer-B1pI6b_M.js → device-explorer-CRG4Utt5.js} +3 -3
  36. package/lib/public/assets/device-explorer-D2AqrrLN.css +1 -0
  37. package/lib/public/assets/{download-x9WP7mcL.js → download-CCDYo_TN.js} +1 -1
  38. package/lib/public/assets/{forgot-password-B0TEH4N-.js → forgot-password-DuLCmVFb.js} +1 -1
  39. package/lib/public/assets/{funnel-Cb40MfFN.js → funnel-CtKNPqzb.js} +1 -1
  40. package/lib/public/assets/{index-BR1wTIPC.js → index-3uxl3o7K.js} +43 -43
  41. package/lib/public/assets/{index-BvH_ekRy.js → index-Zy1Op5C1.js} +1 -1
  42. package/lib/public/assets/{input-DejcCiTI.js → input-CzTWvUXZ.js} +1 -1
  43. package/lib/public/assets/{keycodes-Mavx5iGg.js → keycodes-CI-nYDFM.js} +1 -1
  44. package/lib/public/assets/{layers-BKqYLiHE.js → layers-YZZGFyTX.js} +1 -1
  45. package/lib/public/assets/{lock-LFsFhc17.js → lock-L8tzWgDo.js} +1 -1
  46. package/lib/public/assets/{login-x2pkzt-t.js → login-DBbX-fu8.js} +1 -1
  47. package/lib/public/assets/{maintenance-settings-CYafcoV-.js → maintenance-settings-AJwkOIsE.js} +1 -1
  48. package/lib/public/assets/{monitor-_7_uJ8d9.js → monitor-Di_66F2m.js} +1 -1
  49. package/lib/public/assets/{mouse-pointer-2-BfLuURKN.js → mouse-pointer-2-Cnab2zdg.js} +1 -1
  50. package/lib/public/assets/{network-Z_zBhpsD.js → network-DiSumY-1.js} +1 -1
  51. package/lib/public/assets/{overview-BY5uv08S.js → overview-CwE4gXl5.js} +1 -1
  52. package/lib/public/assets/{page-header-CP2UkykG.js → page-header-CfR4Sm9V.js} +1 -1
  53. package/lib/public/assets/{plus-BTD0dnQ0.js → plus-DOS8P9CQ.js} +1 -1
  54. package/lib/public/assets/{profile-page-BhJZuCS7.js → profile-page-BYpHwQIH.js} +1 -1
  55. package/lib/public/assets/{reset-password-CMN0SO81.js → reset-password-BRVJU_qV.js} +1 -1
  56. package/lib/public/assets/{runbook-page-CBpPX9_5.js → runbook-page-Bi3JvAFZ.js} +1 -1
  57. package/lib/public/assets/{select-Uc9E5pye.js → select-Cqx7pEou.js} +1 -1
  58. package/lib/public/assets/{selector-detail-page-BSyBnRuU.js → selector-detail-page-C0DsncKj.js} +1 -1
  59. package/lib/public/assets/{selector-health-DzqxNkzm.js → selector-health-CAhpdilF.js} +1 -1
  60. package/lib/public/assets/{selector-health-page-CyPLjoD1.js → selector-health-page-CrCVeW2m.js} +1 -1
  61. package/lib/public/assets/{session-detail-page-BZQZN1P-.js → session-detail-page-F_hwYqEt.js} +1 -1
  62. package/lib/public/assets/{settings-CkSWiZq8.js → settings-C-s5pypL.js} +1 -1
  63. package/lib/public/assets/{status-pill-outline-BFYni2qo.js → status-pill-outline-DY5uzv6A.js} +1 -1
  64. package/lib/public/assets/{tablet-BSd-xSZK.js → tablet-Dli7gwyg.js} +1 -1
  65. package/lib/public/assets/{teams-PpoEQz31.js → teams-ui4IhHNC.js} +1 -1
  66. package/lib/public/assets/{trash-2-BorJv5_u.js → trash-2-BV4hSmXJ.js} +1 -1
  67. package/lib/public/assets/{upload-C3i7sAZl.js → upload-DkGy_J-L.js} +1 -1
  68. package/lib/public/assets/{use-password-reset-mode-CeZQ3EsH.js → use-password-reset-mode-BFtMqQOh.js} +1 -1
  69. package/lib/public/assets/{users-Drd7ifJx.js → users-BU0nXIVe.js} +1 -1
  70. package/lib/public/assets/{users-CEJ3fC78.js → users-Nb_rzRmF.js} +1 -1
  71. package/lib/public/assets/{video-off-CNzn6bPE.js → video-off-CUNzD2kH.js} +1 -1
  72. package/lib/public/assets/{webhook-settings-BFqX6QXV.js → webhook-settings-Ca3UFXZe.js} +1 -1
  73. package/lib/public/assets/{zap-4yN8Jdfw.js → zap-B2bH2vKX.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 +11 -104
  82. package/lib/src/app/routers/grid.js +28 -8
  83. package/lib/src/app/routers/nodePhoneControl.js +301 -0
  84. package/lib/src/app/ws/h264StreamWs.js +43 -12
  85. package/lib/src/app/ws/logcatWs.js +48 -14
  86. package/lib/src/app/ws/nodeSocketRelay.js +167 -0
  87. package/lib/src/app/ws/upgradeRouter.js +215 -0
  88. package/lib/src/dashboard/event-manager.js +7 -6
  89. package/lib/src/data-service/device-service.js +152 -49
  90. package/lib/src/data-service/device-store.js +92 -10
  91. package/lib/src/data-service/deviceClaims.js +116 -0
  92. package/lib/src/data-service/deviceFieldOwners.js +32 -10
  93. package/lib/src/data-service/prisma-store.js +102 -31
  94. package/lib/src/device-managers/AndroidDeviceManager.js +40 -42
  95. package/lib/src/device-managers/HealthMonitorService.js +17 -2
  96. package/lib/src/device-managers/IOSDeviceManager.js +5 -1
  97. package/lib/src/device-managers/NodeDevices.js +7 -2
  98. package/lib/src/device-managers/android/AndroidH264StreamService.js +13 -5
  99. package/lib/src/device-managers/android/AndroidStreamService.js +9 -6
  100. package/lib/src/device-managers/android/previewHold.js +2 -2
  101. package/lib/src/device-managers/ios/IOSDiscoveryService.js +19 -8
  102. package/lib/src/device-managers/ios/IOSStreamService.js +162 -40
  103. package/lib/src/device-managers/localDeviceHosts.js +86 -0
  104. package/lib/src/device-managers/ownDeviceRow.js +38 -0
  105. package/lib/src/device-utils.js +78 -24
  106. package/lib/src/gateway/createHandoff.js +73 -0
  107. package/lib/src/gateway/defaultGateway.js +80 -0
  108. package/lib/src/gateway/forwardToNode.js +147 -0
  109. package/lib/src/gateway/hubSessionToken.js +356 -0
  110. package/lib/src/gateway/internalCall.js +90 -0
  111. package/lib/src/gateway/nodeSessionStatus.js +140 -0
  112. package/lib/src/gateway/nodeWebDriverUrl.js +139 -0
  113. package/lib/src/gateway/sessionCreate.js +136 -0
  114. package/lib/src/gateway/sessionGateway.js +201 -0
  115. package/lib/src/gateway/sessionLocator.js +100 -0
  116. package/lib/src/generated/client/edge.js +7 -4
  117. package/lib/src/generated/client/index-browser.js +4 -1
  118. package/lib/src/generated/client/index.d.ts +112 -1
  119. package/lib/src/generated/client/index.js +7 -4
  120. package/lib/src/generated/client/package.json +1 -1
  121. package/lib/src/generated/client/schema.prisma +9 -0
  122. package/lib/src/generated/client/wasm.js +4 -1
  123. package/lib/src/helpers/index.js +5 -1
  124. package/lib/src/logger.js +5 -0
  125. package/lib/src/middleware/authMiddleware.js +80 -0
  126. package/lib/src/middleware/commandAuth.js +31 -17
  127. package/lib/src/middleware/csrfMiddleware.js +3 -2
  128. package/lib/src/middleware/deviceAccessGuard.js +4 -1
  129. package/lib/src/middleware/sessionListingFilter.js +153 -0
  130. package/lib/src/middleware/sessionUpgradeGuard.js +203 -0
  131. package/lib/src/plugin.js +27 -1
  132. package/lib/src/prisma.js +2 -1
  133. package/lib/src/services/DeviceReconciler.js +5 -2
  134. package/lib/src/services/OrphanSweeper.js +12 -19
  135. package/lib/src/services/PortAllocator.js +75 -16
  136. package/lib/src/services/ServerManager.js +63 -30
  137. package/lib/src/services/SessionLifecycleService.js +335 -76
  138. package/lib/src/services/ShutdownCoordinator.js +7 -1
  139. package/lib/src/services/SocketServer.js +11 -2
  140. package/lib/src/services/device-access/LiveSessionOwners.js +53 -0
  141. package/lib/src/services/device-access/SessionOwnerResolver.js +74 -0
  142. package/lib/src/services/device-access/ticketActorAccess.js +3 -1
  143. package/lib/src/services/lease/LeaseOrphanSweeper.js +34 -6
  144. package/lib/src/services/lease/LeaseService.js +19 -5
  145. package/lib/src/services/session/nodeCreateCaps.js +55 -0
  146. package/lib/src/services/session/sessionCredentials.js +7 -59
  147. package/lib/src/sessions/CloudSession.js +12 -0
  148. package/lib/src/sessions/LocalSession.js +52 -26
  149. package/lib/src/sessions/RemoteSession.js +116 -9
  150. package/lib/src/sessions/SessionManager.js +49 -6
  151. package/lib/src/sessions/appiumUmbrella.js +32 -0
  152. package/lib/test/helpers/appium-umbrella.js +84 -0
  153. package/lib/test/helpers/fake-device-table.js +97 -0
  154. package/lib/test/helpers/loki-stores.js +41 -0
  155. package/lib/test/helpers/loopbackRequest.js +77 -0
  156. package/lib/test/helpers/own-node-id.js +29 -0
  157. package/lib/test/helpers/scratch-database.js +100 -0
  158. package/lib/test/helpers/scratch-port-leases.js +119 -0
  159. package/lib/test/helpers/test-container.js +35 -27
  160. package/lib/test/integration/androidDevices.spec.js +5 -4
  161. package/lib/test/integration/ios/01iOSSimulator.spec.js +6 -5
  162. package/lib/test/integration/ios/02iOSDevices.spec.js +3 -2
  163. package/lib/test/integration/prisma-device-store-add-devices.spec.js +14 -4
  164. package/lib/test/setup/scratch-default-database.js +32 -0
  165. package/lib/test/unit/AndroidDeviceManager.spec.js +56 -1
  166. package/lib/test/unit/DeviceModel.spec.js +9 -9
  167. package/lib/test/unit/HealingCascade.test.js +5 -1
  168. package/lib/test/unit/OrphanSweeper.test.js +17 -13
  169. package/lib/test/unit/SessionHeartbeatWrite.test.js +6 -7
  170. package/lib/test/unit/StreamTicketService.spec.js +4 -1
  171. package/lib/test/unit/TokenEndpoint.spec.js +17 -3
  172. package/lib/test/unit/aggregate-hotspots-session-scope.spec.js +5 -5
  173. package/lib/test/unit/android-h264-service.spec.js +24 -0
  174. package/lib/test/unit/android-stream-stall-eof.spec.js +10 -0
  175. package/lib/test/unit/apps-team-visibility.spec.js +16 -16
  176. package/lib/test/unit/auth-me-auth-disabled.test.js +7 -4
  177. package/lib/test/unit/authMiddleware.appTicket.spec.js +5 -5
  178. package/lib/test/unit/authMiddleware.bearer.spec.js +4 -1
  179. package/lib/test/unit/authMiddleware.mcpAudience.spec.js +4 -1
  180. package/lib/test/unit/authMiddleware.streamTicket.spec.js +19 -7
  181. package/lib/test/unit/authMiddleware.test.js +3 -0
  182. package/lib/test/unit/bug-report/route.spec.js +9 -9
  183. package/lib/test/unit/cliargs-admin-only.spec.js +3 -3
  184. package/lib/test/unit/clipboard-route.spec.js +22 -8
  185. package/lib/test/unit/command-auth-appium-server.spec.js +187 -32
  186. package/lib/test/unit/command-auth.spec.js +31 -31
  187. package/lib/test/unit/control-device-lookup.spec.js +9 -9
  188. package/lib/test/unit/control-forward-decision.spec.js +115 -0
  189. package/lib/test/unit/control-lookup-failure.spec.js +3 -3
  190. package/lib/test/unit/create-error-message.spec.js +43 -0
  191. package/lib/test/unit/create-handoff.spec.js +86 -0
  192. package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
  193. package/lib/test/unit/device-access-guard.spec.js +66 -28
  194. package/lib/test/unit/device-service.spec.js +4 -0
  195. package/lib/test/unit/device-store-clear-at-boot.spec.js +48 -0
  196. package/lib/test/unit/device-sync-lost-update.spec.js +17 -89
  197. package/lib/test/unit/device-team-guard.spec.js +20 -20
  198. package/lib/test/unit/device-utils.spec.js +8 -0
  199. package/lib/test/unit/extract-team-cap.spec.js +65 -0
  200. package/lib/test/unit/grid-admin-only.spec.js +3 -3
  201. package/lib/test/unit/healing-tier-gate.spec.js +5 -1
  202. package/lib/test/unit/health-monitor-own-phones.spec.js +102 -0
  203. package/lib/test/unit/health-monitor-stream-lookup.spec.js +11 -1
  204. package/lib/test/unit/helpers.spec.js +27 -0
  205. package/lib/test/unit/hub-boot-sweep-recovery.spec.js +192 -0
  206. package/lib/test/unit/hub-create-once.spec.js +190 -0
  207. package/lib/test/unit/hub-create-session-umbrella.spec.js +160 -0
  208. package/lib/test/unit/hub-gateway-create-appium-server.spec.js +591 -0
  209. package/lib/test/unit/hub-node-control-forward.spec.js +608 -0
  210. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +594 -0
  211. package/lib/test/unit/hub-node-race.spec.js +426 -0
  212. package/lib/test/unit/hub-node-same-host.spec.js +356 -0
  213. package/lib/test/unit/hub-register-removal.spec.js +165 -0
  214. package/lib/test/unit/hub-remote-session-heartbeat.spec.js +277 -0
  215. package/lib/test/unit/hub-routed-session-recovery.spec.js +151 -0
  216. package/lib/test/unit/hub-session-token.spec.js +265 -0
  217. package/lib/test/unit/insert-before-routes.spec.js +5 -5
  218. package/lib/test/unit/install-repository-app-team.spec.js +6 -2
  219. package/lib/test/unit/interceptor/MitmProxyHost.spec.js +23 -10
  220. package/lib/test/unit/internal-call.spec.js +181 -0
  221. package/lib/test/unit/ios-discovery-same-host.spec.js +193 -0
  222. package/lib/test/unit/ios-stream-appium-tunnel.spec.js +317 -0
  223. package/lib/test/unit/ios-stream-port-leases.spec.js +299 -0
  224. package/lib/test/unit/ios-wda-status-identity.spec.js +84 -0
  225. package/lib/test/unit/is-xenon-running.spec.js +69 -0
  226. package/lib/test/unit/lease/LeaseOrphanSweeper.spec.js +2 -2
  227. package/lib/test/unit/lease/LeaseService.spec.js +28 -1
  228. package/lib/test/unit/lease/lease-reap-held-phone.spec.js +154 -0
  229. package/lib/test/unit/lease/lease-sweeper-expiry.spec.js +107 -0
  230. package/lib/test/unit/lease/lease-team-visibility.spec.js +2 -2
  231. package/lib/test/unit/lease/lease-token-log-filter.spec.js +242 -64
  232. package/lib/test/unit/lease/lease-token-oracle.spec.js +20 -7
  233. package/lib/test/unit/lease/session-lease-proof.spec.js +8 -3
  234. package/lib/test/unit/logcat-ws.spec.js +12 -1
  235. package/lib/test/unit/loopback-request.spec.js +81 -0
  236. package/lib/test/unit/mark-node-busy-loki.spec.js +53 -0
  237. package/lib/test/unit/node-create-caps.spec.js +54 -0
  238. package/lib/test/unit/node-gateway-create-appium-server.spec.js +354 -0
  239. package/lib/test/unit/node-hub-session-owner.spec.js +223 -0
  240. package/lib/test/unit/node-phone-control.spec.js +131 -0
  241. package/lib/test/unit/node-socket-relay.spec.js +357 -0
  242. package/lib/test/unit/omni-vision.spec.js +7 -3
  243. package/lib/test/unit/plugin.spec.js +45 -4
  244. package/lib/test/unit/port-allocator-os-probe.spec.js +151 -0
  245. package/lib/test/unit/profile-router.test.js +11 -11
  246. package/lib/test/unit/queue-requester.spec.js +5 -0
  247. package/lib/test/unit/queue-team-scope-routes.spec.js +17 -16
  248. package/lib/test/unit/queue-visibility.spec.js +1 -1
  249. package/lib/test/unit/recording-downloads-visibility.spec.js +2 -2
  250. package/lib/test/unit/recording-instrumentation.spec.js +5 -0
  251. package/lib/test/unit/recording-source-ended.spec.js +5 -0
  252. package/lib/test/unit/recording-store.spec.js +9 -3
  253. package/lib/test/unit/recordings-library-routes.spec.js +51 -51
  254. package/lib/test/unit/recordings-write-visibility.spec.js +2 -2
  255. package/lib/test/unit/remote-session-create.spec.js +130 -0
  256. package/lib/test/unit/reservation-team-visibility.spec.js +5 -5
  257. package/lib/test/unit/reset-link.test.js +14 -14
  258. package/lib/test/unit/session-app-resolution.spec.js +14 -10
  259. package/lib/test/unit/session-assets.spec.js +9 -9
  260. package/lib/test/unit/session-attribution-wiring.spec.js +29 -2
  261. package/lib/test/unit/session-create-layer.spec.js +255 -0
  262. package/lib/test/unit/session-credential-strip.spec.js +25 -9
  263. package/lib/test/unit/session-credentials.spec.js +0 -47
  264. package/lib/test/unit/session-listing-auth.spec.js +345 -0
  265. package/lib/test/unit/session-locator.spec.js +250 -0
  266. package/lib/test/unit/session-node-calls.spec.js +174 -0
  267. package/lib/test/unit/session-owner-resolver.spec.js +107 -0
  268. package/lib/test/unit/session-recovery-routing.spec.js +118 -0
  269. package/lib/test/unit/session-upgrade-guard.spec.js +571 -0
  270. package/lib/test/unit/shutdown-drain-remote.spec.js +58 -0
  271. package/lib/test/unit/socketServer.bearer.spec.js +3 -1
  272. package/lib/test/unit/stream-leave-route.spec.js +19 -7
  273. package/lib/test/unit/stream-own-device-row.spec.js +164 -0
  274. package/lib/test/unit/stream-stop-ownership.spec.js +17 -5
  275. package/lib/test/unit/stream-stop-recording.spec.js +17 -5
  276. package/lib/test/unit/stream-ticket-identity.spec.js +26 -6
  277. package/lib/test/unit/stream-watchdog-timers.spec.js +46 -0
  278. package/lib/test/unit/upgrade-router.spec.js +446 -0
  279. package/lib/test/unit/users-router.test.js +11 -8
  280. package/lib/test/unit/verify-credential.spec.js +4 -1
  281. package/lib/test/unit/webdriver-info-route.spec.js +64 -0
  282. package/lib/test/unit/ws-upgrade-routing-appium-server.spec.js +500 -0
  283. package/lib/tsconfig.tsbuildinfo +1 -1
  284. package/package.json +3 -4
  285. package/prisma/migrations/20260929120000_device_claims/migration.sql +8 -0
  286. package/prisma/schema.prisma +9 -0
  287. package/lib/public/assets/BugReportButton-DKYsOdGV.js +0 -10
  288. package/lib/public/assets/device-explorer-DV8rWRxL.css +0 -1
  289. package/lib/src/proxy/wd-command-proxy.js +0 -221
@@ -13,7 +13,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.removeDevice = removeDevice;
16
- exports.removeDevicesByHost = removeDevicesByHost;
16
+ exports.removeNodeDevices = removeNodeDevices;
17
17
  exports.addNewDevice = addNewDevice;
18
18
  exports.syncDiscoveredDevices = syncDiscoveredDevices;
19
19
  exports.setSimulatorState = setSimulatorState;
@@ -23,6 +23,7 @@ exports.filterRowsByVisibleDevice = filterRowsByVisibleDevice;
23
23
  exports.getDevices = getDevices;
24
24
  exports.getDevice = getDevice;
25
25
  exports.updatedAllocatedDevice = updatedAllocatedDevice;
26
+ exports.claimDeviceForSession = claimDeviceForSession;
26
27
  exports.updateDeviceProgress = updateDeviceProgress;
27
28
  exports.updateCmdExecutedTime = updateCmdExecutedTime;
28
29
  exports.userBlockDevice = userBlockDevice;
@@ -30,6 +31,9 @@ exports.userUnblockDevice = userUnblockDevice;
30
31
  exports.blockDevice = blockDevice;
31
32
  exports.unblockDevice = unblockDevice;
32
33
  exports.unblockDeviceMatchingFilter = unblockDeviceMatchingFilter;
34
+ exports.releaseSessionDevices = releaseSessionDevices;
35
+ exports.releaseSessionDevice = releaseSessionDevice;
36
+ exports.releasePendingClaim = releasePendingClaim;
33
37
  exports.reserveDevice = reserveDevice;
34
38
  exports.releaseReservation = releaseReservation;
35
39
  exports.isDeviceReserved = isDeviceReserved;
@@ -43,6 +47,7 @@ const typedi_1 = require("typedi");
43
47
  const CircuitBreaker_1 = require("./CircuitBreaker");
44
48
  const NotificationService_1 = require("../services/NotificationService");
45
49
  const deviceFieldOwners_1 = require("./deviceFieldOwners");
50
+ const deviceClaims_1 = require("./deviceClaims");
46
51
  const SocketServer_1 = require("../services/SocketServer");
47
52
  const prisma_1 = require("../prisma");
48
53
  const manualLock_1 = require("../services/recording/manualLock");
@@ -63,8 +68,8 @@ const store = new Proxy({}, {
63
68
  function deviceScope(udid, teamId) {
64
69
  return teamId === undefined ? { udid } : { udid, teamId };
65
70
  }
66
- function removeDevice(devices) {
67
- return __awaiter(this, void 0, void 0, function* () {
71
+ function removeDevice(devices_1) {
72
+ return __awaiter(this, arguments, void 0, function* (devices, options = {}) {
68
73
  for (const device of devices) {
69
74
  logger_1.default.info(`Removing device ${device.udid} from host ${device.host}`);
70
75
  const socket = typedi_1.Container.get(SocketServer_1.SocketServer);
@@ -75,18 +80,44 @@ function removeDevice(devices) {
75
80
  const team = socket.hasScopedDashboard()
76
81
  ? yield typedi_1.Container.get(DeviceTeamResolver_1.DeviceTeamResolver).resolve(device.udid)
77
82
  : undefined;
78
- yield store.removeDevices({ udid: device.udid, host: device.host });
83
+ yield store.removeDevices({ udid: device.udid, host: device.host }, options);
79
84
  typedi_1.Container.get(NotificationService_1.NotificationService).dispatchEvent('device_offline', device);
80
85
  void socket.emitToDashboardForDevices('device_removed', device, deviceScope(device.udid, (team === null || team === void 0 ? void 0 : team.known) ? team.teamId : undefined));
81
86
  }
82
87
  });
83
88
  }
84
- function removeDevicesByHost(host) {
89
+ /**
90
+ * The phones a node asks the hub to forget (`POST /register`, `type=remove`
91
+ * for one phone, `type=unregister` when it shuts down). Only that node's: the
92
+ * rows its `nodeId` names when it sends one, else those filed under exactly
93
+ * its `host`, and never one of this server's own (`isOwn`). A udid-less
94
+ * `remove` names no phone and takes nothing.
95
+ *
96
+ * The store matched a host that isn't a URL as a substring, and a remove with
97
+ * no host by udid alone, so a node (an older one sends a bare IP) could delete
98
+ * the hub's own phones, or another node's. Returns how many rows went.
99
+ */
100
+ function removeNodeDevices(removals, isOwn) {
85
101
  return __awaiter(this, void 0, void 0, function* () {
86
- logger_1.default.info(`Removing all devices from host ${host}`);
87
- // We can't easily dispatch events here without fetching first,
88
- // but for now we'll stick to single device removal alerting
89
- yield store.removeDevices({ host });
102
+ const all = yield store.getAllDevices();
103
+ const gone = new Map();
104
+ for (const removal of removals) {
105
+ if (!removal.nodeId && !removal.host)
106
+ continue;
107
+ for (const device of all) {
108
+ if (isOwn(device))
109
+ continue;
110
+ if (removal.udid !== undefined && device.udid !== removal.udid)
111
+ continue;
112
+ const theirs = removal.nodeId
113
+ ? device.nodeId === removal.nodeId
114
+ : device.host === removal.host;
115
+ if (theirs)
116
+ gone.set(`${device.udid}\u0000${device.host}`, device);
117
+ }
118
+ }
119
+ yield removeDevice([...gone.values()].map((d) => ({ udid: d.udid, host: d.host })), { exactHost: true });
120
+ return gone.size;
90
121
  });
91
122
  }
92
123
  function addNewDevice(devices_1, host_1) {
@@ -237,6 +268,22 @@ function updatedAllocatedDevice(device, updateData) {
237
268
  yield store.updateDevice(device.udid, device.host, updateData);
238
269
  });
239
270
  }
271
+ /**
272
+ * Put a created session on the claim its phone was allocated with
273
+ * (`device.claimedAt`), writing `updateData` with it. False, and nothing
274
+ * written, when another session holds the phone now: its claim is not
275
+ * overwritten.
276
+ */
277
+ function claimDeviceForSession(device, sessionId, updateData) {
278
+ return __awaiter(this, void 0, void 0, function* () {
279
+ const claimed = yield store.claimForSession(device.udid, device.host, device.claimedAt, sessionId, updateData);
280
+ if (!claimed) {
281
+ logger_1.default.error(`Session ${sessionId} was created on ${device.udid} at ${device.host}, but another ` +
282
+ 'session has claimed the device since. Leaving that claim in place.');
283
+ }
284
+ return claimed;
285
+ });
286
+ }
240
287
  function updateDeviceProgress(udid_1, host_1, progress_1) {
241
288
  return __awaiter(this, arguments, void 0, function* (udid, host, progress, extra = {}) {
242
289
  logger_1.default.debug(`[${udid}] progress: ${progress}`);
@@ -249,9 +296,9 @@ function updateDeviceProgress(udid_1, host_1, progress_1) {
249
296
  }
250
297
  function updateCmdExecutedTime(sessionId) {
251
298
  return __awaiter(this, void 0, void 0, function* () {
252
- yield store.updateDevices({ session_id: sessionId }, (device) => {
253
- device.lastCmdExecutedAt = new Date().getTime();
254
- });
299
+ // One conditional write. Reading the row and writing all of it back would
300
+ // put back a claim released, or a node's report taken, in between.
301
+ yield store.touchSession(sessionId, new Date().getTime());
255
302
  });
256
303
  }
257
304
  /**
@@ -297,53 +344,109 @@ function blockDevice(udid, host, sessionId) {
297
344
  }, { udid });
298
345
  });
299
346
  }
347
+ /**
348
+ * Free a phone whatever holds it: a manual hold (a preview, a recording) or
349
+ * an admin's release. A session's phone is released with
350
+ * releaseSessionDevices / releaseSessionDevice / releasePendingClaim instead,
351
+ * keyed on its claim.
352
+ *
353
+ * With a host, only that row. A hub also stores its nodes' phones, and a node
354
+ * on the same machine lists the same udid under its own host: matching the
355
+ * udid alone freed that row too, with a session on it.
356
+ */
300
357
  function unblockDevice(udid, host) {
301
358
  return __awaiter(this, void 0, void 0, function* () {
302
- // Prefer udid-only: device.host can be `http://IP:port` while callers pass
303
- // `127.0.0.1` / bindHostOrIp, and a host mismatch silently no-ops (0 rows).
304
- // UDID uniquely identifies a device in this store.
305
- const byUdid = yield store.getDevices({ udid });
306
- if (byUdid.length > 0) {
307
- yield unblockDeviceMatchingFilter({ udid });
308
- return;
359
+ const devices = yield store.findDevices(host === undefined ? { udid } : { udid, host });
360
+ yield forceRelease(devices);
361
+ });
362
+ }
363
+ /** unblockDevice for every phone matching `filter` (getDevices' filter). */
364
+ function unblockDeviceMatchingFilter(filter) {
365
+ return __awaiter(this, void 0, void 0, function* () {
366
+ yield forceRelease(yield store.getDevices(filter));
367
+ });
368
+ }
369
+ function forceRelease(devices) {
370
+ return __awaiter(this, void 0, void 0, function* () {
371
+ yield Promise.all(devices.map((device) => __awaiter(this, void 0, void 0, function* () {
372
+ const totalUtilization = utilizationAfter(device);
373
+ yield (0, device_utils_1.setUtilizationTime)(device.udid, totalUtilization);
374
+ yield store.updateDevice(device.udid, device.host, Object.assign(Object.assign({}, deviceClaims_1.CLAIM_RESET), { busy: false, totalUtilizationTimeMilliSec: totalUtilization }));
375
+ logger_1.default.debug(`Unblocked device ${device.udid}`);
376
+ emitUnblocked(device);
377
+ }))).catch((error) => {
378
+ logger_1.default.error(`Unable to unblock device: ${error}`);
379
+ });
380
+ });
381
+ }
382
+ /**
383
+ * End a session's claim on its phone, keyed on the session's id: a release
384
+ * for a session that has ended, arriving after its phone went to another one,
385
+ * matches nothing. The phone stays busy while its node still reports it busy
386
+ * (deviceClaims.ts). Never throws.
387
+ */
388
+ function releaseSessionDevices(sessionId) {
389
+ return __awaiter(this, void 0, void 0, function* () {
390
+ try {
391
+ const rows = new Map();
392
+ for (const device of [
393
+ ...(yield store.findDevices({ claimSessionId: sessionId })),
394
+ ...(yield store.findDevices({ session_id: sessionId })),
395
+ ]) {
396
+ rows.set(`${device.udid}@${device.host}`, device);
397
+ }
398
+ yield Promise.all([...rows.values()].map((device) => releaseClaimOn(device, { sessionId })));
309
399
  }
310
- if (host) {
311
- yield unblockDeviceMatchingFilter({ udid, host });
400
+ catch (error) {
401
+ logger_1.default.error(`Unable to release the device of session ${sessionId}: ${error}`);
312
402
  }
313
403
  });
314
404
  }
315
- function unblockDeviceMatchingFilter(filter) {
405
+ /** releaseSessionDevices for one phone. */
406
+ function releaseSessionDevice(udid, host, sessionId) {
316
407
  return __awaiter(this, void 0, void 0, function* () {
317
- const devices = yield store.getDevices(filter);
318
- if (devices.length > 0) {
319
- yield Promise.all(devices.map((device) => __awaiter(this, void 0, void 0, function* () {
320
- const sessionStart = device.sessionStartTime;
321
- const currentTime = new Date().getTime();
322
- let utilization = currentTime - sessionStart;
323
- if (sessionStart === 0)
324
- utilization = 0;
325
- const totalUtilization = device.totalUtilizationTimeMilliSec + utilization;
326
- yield (0, device_utils_1.setUtilizationTime)(device.udid, totalUtilization);
327
- yield store.updateDevice(device.udid, device.host, {
328
- session_id: null,
329
- busy: false,
330
- lastCmdExecutedAt: null,
331
- sessionStartTime: 0,
332
- totalUtilizationTimeMilliSec: totalUtilization,
333
- newCommandTimeout: null,
334
- sessionProgress: '',
335
- });
336
- logger_1.default.debug(`Unblocked device ${device.udid}`);
337
- void typedi_1.Container.get(SocketServer_1.SocketServer).emitToDashboardForDevices('device_unblocked', {
338
- udid: device.udid,
339
- host: device.host,
340
- }, deviceScope(device.udid, device.teamId));
341
- }))).catch((error) => {
342
- logger_1.default.error(`Unable to unblock device: ${error}`);
343
- });
408
+ const device = yield store.findDevice({ udid, host });
409
+ return device ? releaseClaimOn(device, { sessionId }) : false;
410
+ });
411
+ }
412
+ /**
413
+ * Give back a phone allocated for a session that was never created: the
414
+ * pending claim it was allocated with (`claimedAt`, as the allocation read
415
+ * it). If that claim has gone (timed out, and the phone taken since) nothing
416
+ * is written.
417
+ */
418
+ function releasePendingClaim(allocated) {
419
+ return __awaiter(this, void 0, void 0, function* () {
420
+ var _a;
421
+ const device = yield store.findDevice({ udid: allocated.udid, host: allocated.host });
422
+ return device ? releaseClaimOn(device, { claimedAt: (_a = allocated.claimedAt) !== null && _a !== void 0 ? _a : null }) : false;
423
+ });
424
+ }
425
+ function releaseClaimOn(device, ref) {
426
+ return __awaiter(this, void 0, void 0, function* () {
427
+ const totalUtilization = utilizationAfter(device);
428
+ const released = yield store.releaseClaim(device.udid, device.host, ref, {
429
+ totalUtilizationTimeMilliSec: totalUtilization,
430
+ });
431
+ if (!released) {
432
+ logger_1.default.info(`Not releasing ${device.udid} at ${device.host}: it no longer holds ${JSON.stringify(ref)}`);
433
+ return false;
344
434
  }
435
+ yield (0, device_utils_1.setUtilizationTime)(device.udid, totalUtilization);
436
+ logger_1.default.debug(`Released ${JSON.stringify(ref)} on ${device.udid}`);
437
+ emitUnblocked(device);
438
+ return true;
345
439
  });
346
440
  }
441
+ /** The phone's total use once the session on it now ends. */
442
+ function utilizationAfter(device) {
443
+ var _a;
444
+ const started = device.sessionStartTime;
445
+ return ((_a = device.totalUtilizationTimeMilliSec) !== null && _a !== void 0 ? _a : 0) + (started ? Date.now() - started : 0);
446
+ }
447
+ function emitUnblocked(device) {
448
+ void typedi_1.Container.get(SocketServer_1.SocketServer).emitToDashboardForDevices('device_unblocked', { udid: device.udid, host: device.host }, deviceScope(device.udid, device.teamId));
449
+ }
347
450
  /**
348
451
  * Reserve a device for exclusive manual use
349
452
  * @param udid Device UDID
@@ -13,6 +13,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.DeviceStoreFactory = void 0;
16
+ const deviceFieldOwners_1 = require("./deviceFieldOwners");
17
+ const deviceClaims_1 = require("./deviceClaims");
16
18
  const logger_1 = __importDefault(require("../logger"));
17
19
  const semver_1 = __importDefault(require("semver"));
18
20
  const db_1 = require("./db");
@@ -153,19 +155,37 @@ class LokiDeviceStore {
153
155
  });
154
156
  });
155
157
  }
156
- updateDevices(filter, updateFn) {
158
+ touchSession(sessionId, at) {
157
159
  return __awaiter(this, void 0, void 0, function* () {
158
- (yield db_1.XenonDatabase.DeviceModel).chain().find(filter).update(updateFn);
160
+ (yield db_1.XenonDatabase.DeviceModel)
161
+ .chain()
162
+ .find({ session_id: sessionId })
163
+ .update((device) => {
164
+ device.lastCmdExecutedAt = at;
165
+ });
159
166
  });
160
167
  }
161
- addDevices(devices) {
162
- return __awaiter(this, void 0, void 0, function* () {
168
+ addDevices(devices_1) {
169
+ return __awaiter(this, arguments, void 0, function* (devices, options = {}) {
163
170
  const deviceModel = yield db_1.XenonDatabase.DeviceModel;
164
171
  const added = [];
165
172
  for (const device of devices) {
166
173
  const existing = deviceModel.findOne({ udid: device.udid, host: device.host });
174
+ // A node's report (PrismaDeviceStore.addNodeReport has the rule).
175
+ const nodeBusy = device.busy === true;
176
+ if (existing && options.nodeReport) {
177
+ Object.assign(existing, (0, deviceFieldOwners_1.pickNodeReportFields)(device), { nodeBusy });
178
+ if (nodeBusy)
179
+ existing.busy = true;
180
+ else if ((0, deviceClaims_1.isUnheld)(existing))
181
+ existing.busy = false;
182
+ deviceModel.update(existing);
183
+ continue;
184
+ }
167
185
  if (!existing) {
168
- const cleanDevice = Object.assign({}, device);
186
+ const cleanDevice = options.nodeReport
187
+ ? Object.assign(Object.assign({}, (0, deviceFieldOwners_1.pickNodeReportFields)(device)), { udid: device.udid, host: device.host, busy: nodeBusy, nodeBusy })
188
+ : Object.assign({}, device);
169
189
  if (cleanDevice.host === undefined)
170
190
  cleanDevice.host = 'Local';
171
191
  if (cleanDevice.userBlocked === undefined)
@@ -185,14 +205,24 @@ class LokiDeviceStore {
185
205
  return added;
186
206
  });
187
207
  }
208
+ // Loki's find() matches a host exactly already, so `exactHost` changes nothing here.
188
209
  removeDevices(filter) {
189
210
  return __awaiter(this, void 0, void 0, function* () {
190
211
  (yield db_1.XenonDatabase.DeviceModel).chain().find(filter).remove();
191
212
  });
192
213
  }
193
- clearStorage() {
214
+ clearStorage(onlyHosts) {
194
215
  return __awaiter(this, void 0, void 0, function* () {
195
- (yield db_1.XenonDatabase.DeviceModel).removeDataOnly();
216
+ const model = yield db_1.XenonDatabase.DeviceModel;
217
+ if (onlyHosts) {
218
+ model
219
+ .chain()
220
+ .find({ host: { $in: [...onlyHosts] } })
221
+ .remove();
222
+ }
223
+ else {
224
+ model.removeDataOnly();
225
+ }
196
226
  });
197
227
  }
198
228
  findDevice(filter) {
@@ -205,8 +235,8 @@ class LokiDeviceStore {
205
235
  return (yield db_1.XenonDatabase.DeviceModel).find(filter);
206
236
  });
207
237
  }
208
- findAndLockDevice(filterOptions) {
209
- return __awaiter(this, void 0, void 0, function* () {
238
+ findAndLockDevice(filterOptions_1) {
239
+ return __awaiter(this, arguments, void 0, function* (filterOptions, options = {}) {
210
240
  // Phase 2: exclude devices held by an active lease.
211
241
  const activeLeases = yield prisma_1.prisma.lease.findMany({
212
242
  where: { status: 'active' },
@@ -216,12 +246,64 @@ class LokiDeviceStore {
216
246
  const devices = yield this.getDevices(filterOptions);
217
247
  const available = devices.find((d) => !d.busy && !d.userBlocked && !blockedKeys.has(`${d.udid}@${d.host}`));
218
248
  if (available) {
219
- yield this.updateDevice(available.udid, available.host, { busy: true });
249
+ yield this.updateDevice(available.udid, available.host, Object.assign({ busy: true }, (options.claim ? { claimedAt: Date.now(), claimSessionId: null } : {})));
220
250
  return available;
221
251
  }
222
252
  return null;
223
253
  });
224
254
  }
255
+ claimForSession(udid, host, claimedAt, sessionId, update) {
256
+ return __awaiter(this, void 0, void 0, function* () {
257
+ const model = yield db_1.XenonDatabase.DeviceModel;
258
+ const device = model.findOne({ udid, host });
259
+ if (!device || device.claimSessionId != null)
260
+ return false;
261
+ if (device.claimedAt != null && device.claimedAt !== claimedAt)
262
+ return false;
263
+ Object.assign(device, update, {
264
+ busy: true,
265
+ claimSessionId: sessionId,
266
+ claimedAt: claimedAt !== null && claimedAt !== void 0 ? claimedAt : Date.now(),
267
+ });
268
+ model.update(device);
269
+ return true;
270
+ });
271
+ }
272
+ releaseClaim(udid, host, ref, update) {
273
+ return __awaiter(this, void 0, void 0, function* () {
274
+ const model = yield db_1.XenonDatabase.DeviceModel;
275
+ const device = model.findOne({ udid, host });
276
+ if (!device || !(0, deviceClaims_1.holdsClaim)(device, ref))
277
+ return false;
278
+ Object.assign(device, update, deviceClaims_1.CLAIM_RESET);
279
+ if ((0, deviceClaims_1.isUnheld)(device))
280
+ device.busy = false;
281
+ model.update(device);
282
+ return true;
283
+ });
284
+ }
285
+ releaseLeaseLock(udid, host) {
286
+ return __awaiter(this, void 0, void 0, function* () {
287
+ const model = yield db_1.XenonDatabase.DeviceModel;
288
+ const device = model.findOne({ udid, host });
289
+ if (!(device === null || device === void 0 ? void 0 : device.busy) || !(0, deviceClaims_1.isUnheld)(device))
290
+ return false;
291
+ device.busy = false;
292
+ model.update(device);
293
+ return true;
294
+ });
295
+ }
296
+ markNodeBusy(udid, host) {
297
+ return __awaiter(this, void 0, void 0, function* () {
298
+ const model = yield db_1.XenonDatabase.DeviceModel;
299
+ const device = model.findOne({ udid, host });
300
+ if (!device)
301
+ return;
302
+ device.nodeBusy = true;
303
+ device.busy = true;
304
+ model.update(device);
305
+ });
306
+ }
225
307
  resetMetrics() {
226
308
  return __awaiter(this, void 0, void 0, function* () {
227
309
  (yield db_1.XenonDatabase.DeviceModel)
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CLAIM_RESET = exports.UNHELD = exports.NO_CLAIM = exports.PENDING_CLAIM_TIMEOUT_MS = void 0;
4
+ exports.isPendingClaim = isPendingClaim;
5
+ exports.pendingClaimExpired = pendingClaimExpired;
6
+ exports.claimWhere = claimWhere;
7
+ exports.holdsClaim = holdsClaim;
8
+ exports.hasNoClaim = hasNoClaim;
9
+ exports.isUnheld = isUnheld;
10
+ exports.heldHere = heldHere;
11
+ /**
12
+ * Why a phone is busy, on the server that allocates it.
13
+ *
14
+ * Allocation reads `busy` (findAndLockDevice takes a phone only where it is
15
+ * false, in one conditional update). On a hub a node's phone is busy for one
16
+ * of two reasons, each kept in its own columns so neither can erase the
17
+ * other:
18
+ *
19
+ * - **The claim**, this server's: `claimedAt` (when allocation took the phone)
20
+ * and `claimSessionId` (the session's id, written when the session exists).
21
+ * A claim with no session id yet is *pending*: its session is being
22
+ * created, which can take minutes on a first driver install.
23
+ * - **The node's report**: `nodeBusy`, what the node last said
24
+ * (`POST /xenon/api/register`). Always false for a phone this server drives.
25
+ *
26
+ * `busy` is claim OR report. No write that can clear it reads the row first:
27
+ *
28
+ * - A node reporting its phone free clears `busy` only where nothing of the
29
+ * hub's holds it (UNHELD: no claim, and no hold such as a preview or a
30
+ * recording in `session_id`), in one conditional update. A report sent
31
+ * before the hub's claim (every 30 s by default) can no longer undo it.
32
+ * - A release names the claim it ends (ClaimRef): a session's id, or when a
33
+ * pending claim was taken. A stale release, for a session that ended while
34
+ * the phone went on to another, matches nothing. Then `busy` is cleared
35
+ * only where nothing else holds the phone (UNHELD), again one conditional
36
+ * update: a node still reporting it busy keeps it busy.
37
+ * - The idle sweeper leaves a pending claim alone until
38
+ * PENDING_CLAIM_TIMEOUT_MS, whatever the new-command timeout.
39
+ *
40
+ * A server with no nodes (standalone, or a node itself) never has `nodeBusy`
41
+ * set, so `busy` is its claim as it always was.
42
+ *
43
+ * Only a session's allocation takes a claim. A lease locks with `busy` alone,
44
+ * as before; a manual hold (a preview, a recording) writes `session_id`.
45
+ */
46
+ /**
47
+ * How long a claim may wait for its session to be created before the idle
48
+ * sweeper frees it: a create that never finished (the hub restarted during
49
+ * it, or it hung). Well past a create's own budget: the phone's wait
50
+ * (`deviceAvailabilityTimeoutMs`) comes before the claim, and a first
51
+ * UiAutomator2 or WebDriverAgent install fits in a few minutes.
52
+ */
53
+ exports.PENDING_CLAIM_TIMEOUT_MS = 10 * 60000;
54
+ /** A claim whose session is still being created. */
55
+ function isPendingClaim(device) {
56
+ return device.claimedAt != null && device.claimSessionId == null;
57
+ }
58
+ /** A pending claim older than PENDING_CLAIM_TIMEOUT_MS. */
59
+ function pendingClaimExpired(device, now) {
60
+ return isPendingClaim(device) && now - device.claimedAt > exports.PENDING_CLAIM_TIMEOUT_MS;
61
+ }
62
+ /** Prisma `where` (beside udid and host) for the claim `ref` names. */
63
+ function claimWhere(ref) {
64
+ if ('sessionId' in ref) {
65
+ return {
66
+ OR: [{ claimSessionId: ref.sessionId }, { claimSessionId: null, session_id: ref.sessionId }],
67
+ };
68
+ }
69
+ if (ref.claimedAt == null)
70
+ return { claimSessionId: null, claimedAt: null, session_id: null };
71
+ return { claimSessionId: null, claimedAt: ref.claimedAt };
72
+ }
73
+ /** claimWhere as a predicate, for the in-memory store. */
74
+ function holdsClaim(device, ref) {
75
+ if ('sessionId' in ref) {
76
+ return (device.claimSessionId === ref.sessionId ||
77
+ (device.claimSessionId == null && device.session_id === ref.sessionId));
78
+ }
79
+ if (device.claimSessionId != null)
80
+ return false;
81
+ if (ref.claimedAt == null)
82
+ return device.claimedAt == null && device.session_id == null;
83
+ return device.claimedAt === ref.claimedAt;
84
+ }
85
+ /** No claim of this server's on the phone, pending or not. */
86
+ exports.NO_CLAIM = { claimSessionId: null, claimedAt: null };
87
+ function hasNoClaim(device) {
88
+ return device.claimSessionId == null && device.claimedAt == null;
89
+ }
90
+ /**
91
+ * Nothing holds the phone: no claim, no session or manual hold, and no node
92
+ * reporting it busy. Only then may a release clear `busy`.
93
+ */
94
+ exports.UNHELD = Object.assign(Object.assign({}, exports.NO_CLAIM), { session_id: null, nodeBusy: false });
95
+ function isUnheld(device) {
96
+ return hasNoClaim(device) && device.session_id == null && device.nodeBusy !== true;
97
+ }
98
+ /**
99
+ * Busy for a reason this server has on record: its claim, a session or hold
100
+ * in `session_id`, or plain `busy` on a phone of its own. False for a node's
101
+ * phone busy only by the node's report (`nodeBusy`), a preview the node
102
+ * holds say: this server knows no holder for it, and the node, which does,
103
+ * judges every call forwarded there with its own guard. An ownership check
104
+ * reads this, not `busy`, or it fails closed on every such phone, refusing
105
+ * the preview's own holder. A server with no nodes never has nodeBusy, so
106
+ * for it this is `busy`.
107
+ */
108
+ function heldHere(device) {
109
+ if (!device.busy)
110
+ return false;
111
+ if (device.nodeBusy !== true)
112
+ return true;
113
+ return device.session_id != null || device.claimedAt != null || device.claimSessionId != null;
114
+ }
115
+ /** What ending a claim resets: the claim, and the session's bookkeeping. */
116
+ exports.CLAIM_RESET = Object.assign(Object.assign({}, exports.NO_CLAIM), { session_id: null, lastCmdExecutedAt: null, sessionStartTime: 0, newCommandTimeout: null, sessionProgress: '', owningSessionId: null, lockedAt: null });
@@ -3,8 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.DISCOVERY_FIELDS = void 0;
6
+ exports.NODE_REPORT_FIELDS = exports.DISCOVERY_FIELDS = void 0;
7
7
  exports.pickDiscoveryFields = pickDiscoveryFields;
8
+ exports.pickNodeReportFields = pickNodeReportFields;
8
9
  exports.discoveryChanges = discoveryChanges;
9
10
  const lodash_1 = __importDefault(require("lodash"));
10
11
  /**
@@ -12,12 +13,21 @@ const lodash_1 = __importDefault(require("lodash"));
12
13
  *
13
14
  * The periodic device sync reads every row, spends seconds in adb or simctl,
14
15
  * then writes each phone back. For a phone it already knows it may write only
15
- * what discovery itself observes (`discovery`). Every `runtime` column has
16
+ * what discovery itself observes (`discovery`). Every other column has
16
17
  * another writer: a session's lock, a preview's hold, an admin's block,
17
18
  * reservation or team, a counter, the health check. A sync that read the row
18
19
  * before one of those writes would put the old value back. That erased a live
19
20
  * preview hold on the lab, and could hand a phone in a session to a second one.
20
21
  *
22
+ * A hub also stores its nodes' phones, from each node's report
23
+ * (`POST /xenon/api/register`, NODE_REPORT_FIELDS). The report carries what
24
+ * the node observes of its own phone: `discovery`, and `observed` (its health,
25
+ * which only the server driving the phone can check). It never writes a
26
+ * `runtime` column: the hub's claim for a session, and the settings the hub
27
+ * owns for the phone (team, tags, reservation, block). Whether the node says
28
+ * the phone is busy goes to `nodeBusy` (`report`), never to `busy`
29
+ * (deviceClaims.ts).
30
+ *
21
31
  * Typed over every column, so a new column won't compile until it is placed.
22
32
  */
23
33
  const OWNER = {
@@ -59,8 +69,9 @@ const OWNER = {
59
69
  newCommandTimeout: 'runtime',
60
70
  owningSessionId: 'runtime',
61
71
  lockedAt: 'runtime',
72
+ claimSessionId: 'runtime',
73
+ claimedAt: 'runtime',
62
74
  userBlocked: 'runtime',
63
- offline: 'runtime',
64
75
  reservationReason: 'runtime',
65
76
  reservedBy: 'runtime',
66
77
  reservedUntil: 'runtime',
@@ -68,14 +79,16 @@ const OWNER = {
68
79
  tags: 'runtime',
69
80
  totalUtilizationTimeMilliSec: 'runtime',
70
81
  total_session_count: 'runtime',
71
- totalHealedCount: 'runtime',
72
82
  dashboard_link: 'runtime',
73
- healthStatus: 'runtime',
74
- healthCheckError: 'runtime',
75
- lastHealthCheckAt: 'runtime',
76
- batteryLevel: 'runtime',
77
- thermalStatus: 'runtime',
78
- storageFree: 'runtime',
83
+ offline: 'observed',
84
+ totalHealedCount: 'observed',
85
+ healthStatus: 'observed',
86
+ healthCheckError: 'observed',
87
+ lastHealthCheckAt: 'observed',
88
+ batteryLevel: 'observed',
89
+ thermalStatus: 'observed',
90
+ storageFree: 'observed',
91
+ nodeBusy: 'report',
79
92
  };
80
93
  /** The columns a sync may write to a phone it already knows. */
81
94
  exports.DISCOVERY_FIELDS = new Set(Object.keys(OWNER).filter((column) => OWNER[column] === 'discovery'));
@@ -83,6 +96,15 @@ exports.DISCOVERY_FIELDS = new Set(Object.keys(OWNER).filter((column) => OWNER[c
83
96
  function pickDiscoveryFields(data) {
84
97
  return lodash_1.default.pickBy(data, (_value, key) => exports.DISCOVERY_FIELDS.has(key));
85
98
  }
99
+ /**
100
+ * The columns a hub takes from a node's report of a phone: what the node
101
+ * observes of it. Not `nodeBusy`, which the hub sets from the report's `busy`.
102
+ */
103
+ exports.NODE_REPORT_FIELDS = new Set(Object.keys(OWNER).filter((column) => OWNER[column] === 'discovery' || OWNER[column] === 'observed'));
104
+ /** Only the columns of `data` a hub takes from a node's report. */
105
+ function pickNodeReportFields(data) {
106
+ return lodash_1.default.pickBy(data, (_value, key) => exports.NODE_REPORT_FIELDS.has(key));
107
+ }
86
108
  /**
87
109
  * What a sync writes to a phone it already knows: the discovery columns whose
88
110
  * value differs from `known`, the row the sync started from. Discovery hands