@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
@@ -14,8 +14,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.RemoteSession = void 0;
16
16
  const axios_1 = __importDefault(require("axios"));
17
+ const typedi_1 = require("typedi");
17
18
  const logger_1 = __importDefault(require("../logger"));
18
19
  const SessionType_1 = __importDefault(require("../enums/SessionType"));
20
+ const hubSessionToken_1 = require("../gateway/hubSessionToken");
21
+ const nodeSessionStatus_1 = require("../gateway/nodeSessionStatus");
19
22
  const XenonSession_1 = require("./XenonSession");
20
23
  class RemoteSession extends XenonSession_1.XenonSession {
21
24
  constructor(options) {
@@ -29,8 +32,30 @@ class RemoteSession extends XenonSession_1.XenonSession {
29
32
  getType() {
30
33
  return SessionType_1.default.REMOTE;
31
34
  }
35
+ /** The WebDriver base URL of the server that runs this session. */
36
+ getWebDriverUrl() {
37
+ return this.baseUrl;
38
+ }
39
+ /**
40
+ * What every call about this session carries. A node gets the hub's token
41
+ * for this session, which it accepts in place of the client's credentials
42
+ * when its own per-command auth is on (the hub checked those).
43
+ */
44
+ callOptions() {
45
+ return __awaiter(this, void 0, void 0, function* () {
46
+ const token = yield typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer).tokenFor(this.sessionId);
47
+ return token ? { headers: { [hubSessionToken_1.HUB_TOKEN_HEADER]: token } } : {};
48
+ });
49
+ }
50
+ call(config) {
51
+ return __awaiter(this, void 0, void 0, function* () {
52
+ var _a, _b;
53
+ const options = yield this.callOptions();
54
+ return (0, axios_1.default)(Object.assign(Object.assign(Object.assign({}, config), options), { headers: Object.assign(Object.assign({}, ((_a = config.headers) !== null && _a !== void 0 ? _a : {})), ((_b = options.headers) !== null && _b !== void 0 ? _b : {})) }));
55
+ });
56
+ }
32
57
  getScreenShot() {
33
- return (0, axios_1.default)({
58
+ return this.call({
34
59
  method: 'get',
35
60
  url: `${this.baseUrl}/session/${this.sessionId}/screenshot`,
36
61
  }).then((response) => (response.data ? response.data.value : ''));
@@ -41,7 +66,7 @@ class RemoteSession extends XenonSession_1.XenonSession {
41
66
  // Generous timeout on purpose: serialising a deep hierarchy on a slow
42
67
  // real device is seconds, not milliseconds, and a truncated read here
43
68
  // reads to the caller as "this screen has no elements".
44
- const response = yield (0, axios_1.default)({
69
+ const response = yield this.call({
45
70
  method: 'get',
46
71
  url: `${this.baseUrl}/session/${this.sessionId}/source`,
47
72
  timeout: 60000,
@@ -54,7 +79,7 @@ class RemoteSession extends XenonSession_1.XenonSession {
54
79
  var _a, _b, _c, _d, _e, _f, _g;
55
80
  logger_1.default.info(`[RemoteSession] stopVideoRecording called for session ${this.sessionId}. isVideoAvailable: ${this.isVideoAvailable}`);
56
81
  try {
57
- const response = yield (0, axios_1.default)({
82
+ const response = yield this.call({
58
83
  method: 'post',
59
84
  url: `${this.baseUrl}/session/${this.sessionId}/appium/stop_recording_screen`,
60
85
  data: {},
@@ -67,7 +92,7 @@ class RemoteSession extends XenonSession_1.XenonSession {
67
92
  logger_1.default.warn(`[RemoteSession] stopVideoRecording error: ${err.message}, trying anyway to retrieve video...`);
68
93
  // Even if there's an error, try to get the video data
69
94
  try {
70
- const retryResponse = yield (0, axios_1.default)({
95
+ const retryResponse = yield this.call({
71
96
  method: 'post',
72
97
  url: `${this.baseUrl}/session/${this.sessionId}/appium/stop_recording_screen`,
73
98
  data: {},
@@ -88,7 +113,7 @@ class RemoteSession extends XenonSession_1.XenonSession {
88
113
  var _a, _b;
89
114
  logger_1.default.info(`[RemoteSession] stopPerformanceRecording called for session ${this.sessionId}`);
90
115
  try {
91
- const response = yield (0, axios_1.default)({
116
+ const response = yield this.call({
92
117
  method: 'post',
93
118
  url: `${this.baseUrl}/session/${this.sessionId}/execute/sync`,
94
119
  data: {
@@ -112,7 +137,7 @@ class RemoteSession extends XenonSession_1.XenonSession {
112
137
  return __awaiter(this, void 0, void 0, function* () {
113
138
  logger_1.default.info(`[RemoteSession] startPerformanceRecording called for session ${this.sessionId}`);
114
139
  try {
115
- yield (0, axios_1.default)({
140
+ yield this.call({
116
141
  method: 'post',
117
142
  url: `${this.baseUrl}/session/${this.sessionId}/execute/sync`,
118
143
  data: {
@@ -159,7 +184,7 @@ class RemoteSession extends XenonSession_1.XenonSession {
159
184
  size = '720x1280';
160
185
  }
161
186
  logger_1.default.info(`[RemoteSession] Starting recording with resolution: ${resolution}, size: ${size}`);
162
- return (0, axios_1.default)({
187
+ return this.call({
163
188
  method: 'post',
164
189
  url: `${this.baseUrl}/session/${this.sessionId}/appium/start_recording_screen`,
165
190
  data: {
@@ -196,9 +221,17 @@ class RemoteSession extends XenonSession_1.XenonSession {
196
221
  return null;
197
222
  }
198
223
  }
224
+ /**
225
+ * Whether the node still has this session. The node is asked with its
226
+ * session-status route (gateway/nodeSessionStatus.ts), which reads Appium's
227
+ * umbrella and runs no command: a WebDriver probe every ~30 s restarted the
228
+ * node driver's new-command timeout and Xenon's idle clock there, so an
229
+ * abandoned session and its phone were kept alive for ever. A node without
230
+ * the route (an older Xenon) is probed the old way, and the hub says so once.
231
+ */
199
232
  checkHealth() {
200
233
  return __awaiter(this, void 0, void 0, function* () {
201
- var _a, _b, _c, _d;
234
+ var _a;
202
235
  if (!this.sessionId) {
203
236
  return {
204
237
  isHealthy: false,
@@ -206,12 +239,86 @@ class RemoteSession extends XenonSession_1.XenonSession {
206
239
  message: 'No session ID assigned',
207
240
  };
208
241
  }
242
+ return (_a = (yield this.askNodeForSession())) !== null && _a !== void 0 ? _a : this.probeWithCommand();
243
+ });
244
+ }
245
+ /**
246
+ * The node's answer at its session-status route, or null when it has no
247
+ * such route and the WebDriver probe has to do.
248
+ */
249
+ askNodeForSession() {
250
+ return __awaiter(this, void 0, void 0, function* () {
251
+ var _a, _b, _c, _d, _e;
252
+ let origin;
253
+ try {
254
+ origin = new URL(this.baseUrl).origin;
255
+ }
256
+ catch (_f) {
257
+ return null;
258
+ }
259
+ const support = typedi_1.Container.get(nodeSessionStatus_1.NodeSessionProbeSupport);
260
+ if (!support.shouldAsk(origin))
261
+ return null;
262
+ let response;
263
+ try {
264
+ response = yield this.call({
265
+ method: 'get',
266
+ url: `${origin}${nodeSessionStatus_1.NODE_SESSION_STATUS_PATH}/${encodeURIComponent(this.sessionId)}`,
267
+ timeout: 5000,
268
+ validateStatus: () => true,
269
+ });
270
+ }
271
+ catch (err) {
272
+ const unreachable = (err === null || err === void 0 ? void 0 : err.code) === 'ECONNREFUSED' || (err === null || err === void 0 ? void 0 : err.code) === 'ETIMEDOUT';
273
+ return {
274
+ isHealthy: false,
275
+ errorType: unreachable ? XenonSession_1.HealthErrorType.SERVER_UNREACHABLE : XenonSession_1.HealthErrorType.TIMEOUT,
276
+ message: `Node session status failed: ${(_b = (_a = err === null || err === void 0 ? void 0 : err.code) !== null && _a !== void 0 ? _a : err === null || err === void 0 ? void 0 : err.message) !== null && _b !== void 0 ? _b : err}`,
277
+ };
278
+ }
279
+ if (!((_c = response.headers) === null || _c === void 0 ? void 0 : _c[nodeSessionStatus_1.NODE_SESSION_STATUS_HEADER])) {
280
+ support.unsupported(origin, response.status);
281
+ return null;
282
+ }
283
+ const exists = (_e = (_d = response.data) === null || _d === void 0 ? void 0 : _d.value) === null || _e === void 0 ? void 0 : _e.exists;
284
+ if (response.status === 200 && typeof exists === 'boolean') {
285
+ if (exists)
286
+ return { isHealthy: true, errorType: XenonSession_1.HealthErrorType.NONE, statusCode: 200 };
287
+ return {
288
+ isHealthy: false,
289
+ errorType: XenonSession_1.HealthErrorType.SESSION_NOT_FOUND,
290
+ message: 'The node no longer has this session',
291
+ statusCode: 404,
292
+ };
293
+ }
294
+ // The node refused the hub's token for it, as the WebDriver probe's
295
+ // unknown-session answer would.
296
+ if (response.status === 404) {
297
+ return {
298
+ isHealthy: false,
299
+ errorType: XenonSession_1.HealthErrorType.SESSION_NOT_FOUND,
300
+ message: "The node refused the hub's token for this session",
301
+ statusCode: 404,
302
+ };
303
+ }
304
+ return {
305
+ isHealthy: false,
306
+ errorType: XenonSession_1.HealthErrorType.DRIVER_ERROR,
307
+ message: `Node session status answered ${response.status}`,
308
+ statusCode: response.status,
309
+ };
310
+ });
311
+ }
312
+ /** The probe for a node without the session-status route: a WebDriver command. */
313
+ probeWithCommand() {
314
+ return __awaiter(this, void 0, void 0, function* () {
315
+ var _a, _b, _c, _d;
209
316
  // Identify the root Appium URL (strip /wd-internal if present)
210
317
  const appiumUrl = this.baseUrl.replace(/\/wd-internal$/, '');
211
318
  try {
212
319
  // 1️⃣ Safe, read-only, lightweight probe: GET /session/{id}/timeouts
213
320
  // This is a W3C standard command that all Appium drivers should register.
214
- const response = yield (0, axios_1.default)({
321
+ const response = yield this.call({
215
322
  method: 'get',
216
323
  url: `${this.baseUrl}/session/${this.sessionId}/timeouts`,
217
324
  timeout: 5000,
@@ -19,13 +19,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
19
19
  };
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
21
  exports.SESSION_MANAGER = exports.SessionManager = void 0;
22
+ const os_1 = __importDefault(require("os"));
22
23
  const typedi_1 = require("typedi");
23
24
  const prisma_1 = require("../prisma");
24
25
  const logger_1 = __importDefault(require("../logger"));
25
26
  const RemoteSession_1 = require("./RemoteSession");
26
27
  const CloudSession_1 = require("./CloudSession");
27
28
  const device_store_1 = require("../data-service/device-store");
28
- const helpers_1 = require("../helpers");
29
+ const nodeWebDriverUrl_1 = require("../gateway/nodeWebDriverUrl");
29
30
  const XenonCapabilityManager_1 = require("../XenonCapabilityManager");
30
31
  /**
31
32
  * SessionManager with persistence and recovery capabilities.
@@ -81,8 +82,9 @@ let SessionManager = class SessionManager {
81
82
  * - Sessions on REMOTE nodes are recovered as RemoteSessions.
82
83
  * - Sessions on CLOUD providers are recovered as CloudSessions.
83
84
  */
84
- recoverActiveSessions(currentNodeId, nodeBasePath) {
85
- return __awaiter(this, void 0, void 0, function* () {
85
+ recoverActiveSessions(currentNodeId_1, nodeBasePath_1) {
86
+ return __awaiter(this, arguments, void 0, function* (currentNodeId, nodeBasePath, isLocalHost = () => false) {
87
+ var _a, _b;
86
88
  this.log.info('🔄 Attempting to recover active sessions from database...');
87
89
  try {
88
90
  // Find all sessions that are still "running" (not ended)
@@ -111,8 +113,13 @@ let SessionManager = class SessionManager {
111
113
  failedCount++;
112
114
  continue;
113
115
  }
114
- // Check if this session was on THE CURRENT NODE (LocalSession)
115
- const isLocalSession = dbSession.node_id === currentNodeId || device.nodeId === currentNodeId;
116
+ // Check if this session was on THE CURRENT NODE (LocalSession). The
117
+ // node id is new on every boot, so a phone this server drives is
118
+ // also known by its host; otherwise a local session from before the
119
+ // restart would be rebuilt as a remote one pointing back at this hub.
120
+ const isLocalSession = dbSession.node_id === currentNodeId ||
121
+ device.nodeId === currentNodeId ||
122
+ (!device.cloud && isLocalHost(device.host));
116
123
  if (isLocalSession) {
117
124
  // LocalSessions cannot be recovered - the Appium driver is gone
118
125
  this.log.warn(`⏹️ Session ${dbSession.id}: Was a LocalSession on this node. Marking as failed (driver lost).`);
@@ -144,7 +151,8 @@ let SessionManager = class SessionManager {
144
151
  alwaysMatch: desiredCaps,
145
152
  firstMatch: [{}],
146
153
  });
147
- const baseUrl = (0, helpers_1.nodeUrl)(device, nodeBasePath);
154
+ // The node's own base path, which need not be this hub's.
155
+ const baseUrl = yield (0, nodeWebDriverUrl_1.nodeWebDriverUrl)(device, nodeBasePath);
148
156
  // Create the appropriate session type
149
157
  let recoveredSession;
150
158
  if (device.cloud) {
@@ -169,8 +177,12 @@ let SessionManager = class SessionManager {
169
177
  });
170
178
  this.log.info(`🌐 Recovered RemoteSession ${dbSession.id} on node ${device.nodeId}`);
171
179
  }
180
+ // Its owner, as the row recorded it.
181
+ recoveredSession.apiKeyId = (_a = dbSession.api_key_id) !== null && _a !== void 0 ? _a : null;
182
+ recoveredSession.userId = (_b = dbSession.user_id) !== null && _b !== void 0 ? _b : null;
172
183
  // Add to in-memory map
173
184
  this.addSession(dbSession.id, recoveredSession);
185
+ yield this.adoptHeartbeat(dbSession.id);
174
186
  recoveredCount++;
175
187
  }
176
188
  catch (sessionErr) {
@@ -188,6 +200,37 @@ let SessionManager = class SessionManager {
188
200
  }
189
201
  });
190
202
  }
203
+ /**
204
+ * A recovered session is this process's from now on: its row's heartbeat
205
+ * is stamped with this process, now. The row still carried the heartbeat
206
+ * of the process that ran before the restart, so after an outage longer
207
+ * than 3 heartbeat intervals the orphan sweep (OrphanSweeper, at boot and
208
+ * on every interval) took it for an orphan: it failed the session recovery
209
+ * had just rebuilt and freed its phone, while the node kept the session.
210
+ *
211
+ * Stamping, rather than handing the sweeps a list of ids to skip, makes the
212
+ * row say what is true, so every reader of it agrees: both sweeps, and the
213
+ * heartbeat's own check for rows nobody updates. From here the heartbeat
214
+ * keeps it fresh, and ends the session if its node no longer has it. A
215
+ * failed write is logged; the heartbeat's next write repairs it.
216
+ */
217
+ adoptHeartbeat(sessionId) {
218
+ return __awaiter(this, void 0, void 0, function* () {
219
+ try {
220
+ yield prisma_1.prisma.session.update({
221
+ where: { id: sessionId },
222
+ data: {
223
+ last_heartbeat_at: new Date(),
224
+ heartbeat_pid: process.pid,
225
+ heartbeat_host: os_1.default.hostname(),
226
+ },
227
+ });
228
+ }
229
+ catch (err) {
230
+ this.log.warn(`Could not stamp recovered session ${sessionId}'s heartbeat: ${err.message}`);
231
+ }
232
+ });
233
+ }
191
234
  /**
192
235
  * Mark a session as failed in the database
193
236
  */
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.AppiumUmbrella = void 0;
10
+ const typedi_1 = require("typedi");
11
+ function isUmbrella(driver) {
12
+ return (!!driver &&
13
+ typeof driver === 'object' &&
14
+ typeof driver.sessionExists === 'function');
15
+ }
16
+ let AppiumUmbrella = class AppiumUmbrella {
17
+ /** Remember the umbrella a create was given. Anything else is ignored. */
18
+ note(driver) {
19
+ if (isUmbrella(driver))
20
+ this.umbrella = driver;
21
+ }
22
+ /** Whether Appium has this session now, without sending it a command. */
23
+ hasSession(sessionId) {
24
+ if (!sessionId || !this.umbrella)
25
+ return false;
26
+ return !!this.umbrella.sessionExists(sessionId);
27
+ }
28
+ };
29
+ exports.AppiumUmbrella = AppiumUmbrella;
30
+ exports.AppiumUmbrella = AppiumUmbrella = __decorate([
31
+ (0, typedi_1.Service)()
32
+ ], AppiumUmbrella);
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.FAKE_AUTOMATION = void 0;
16
+ exports.appiumDir = appiumDir;
17
+ exports.appiumBaseDriver = appiumBaseDriver;
18
+ exports.quietAppiumLogs = quietAppiumLogs;
19
+ exports.fakeInnerDriverClass = fakeInnerDriverClass;
20
+ exports.umbrellaWith = umbrellaWith;
21
+ const path_1 = __importDefault(require("path"));
22
+ /**
23
+ * Appium's own umbrella driver (`AppiumDriver`) and base-driver, as installed
24
+ * with the `appium` devDependency, for specs that run a real Appium 3
25
+ * `server()` in-process. Xenon's own `@appium/base-driver` is an older copy;
26
+ * these are the ones Appium runs.
27
+ */
28
+ function appiumDir() {
29
+ return path_1.default.dirname(require.resolve('appium/package.json'));
30
+ }
31
+ function appiumBaseDriver() {
32
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
33
+ return require(require.resolve('@appium/base-driver', { paths: [appiumDir()] }));
34
+ }
35
+ /** Silence Appium's own logger (its HTTP log writes every request) until restore(). */
36
+ function quietAppiumLogs() {
37
+ appiumBaseDriver();
38
+ const support = require.resolve('@appium/support', { paths: [appiumDir()] });
39
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
40
+ const log = require(require.resolve('@appium/logger', { paths: [support] })).default;
41
+ const level = log.level;
42
+ log.level = 'silent';
43
+ return {
44
+ restore: () => {
45
+ log.level = level;
46
+ },
47
+ };
48
+ }
49
+ /** The automationName the fake inner driver answers to. */
50
+ exports.FAKE_AUTOMATION = 'XenonTestFake';
51
+ let fakeInnerDriver;
52
+ /**
53
+ * A driver the umbrella can start a session with: Appium's own BaseDriver,
54
+ * which validates the capabilities and makes a session id, and nothing more.
55
+ */
56
+ function fakeInnerDriverClass() {
57
+ if (!fakeInnerDriver) {
58
+ const { BaseDriver } = appiumBaseDriver();
59
+ fakeInnerDriver = class XenonTestFakeDriver extends BaseDriver {
60
+ };
61
+ }
62
+ return fakeInnerDriver;
63
+ }
64
+ /**
65
+ * Appium's umbrella driver with these plugin classes, which finds the fake
66
+ * inner driver for `appium:automationName` FAKE_AUTOMATION and, like Appium,
67
+ * no driver for anything else.
68
+ */
69
+ function umbrellaWith(plugins) {
70
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
71
+ const { AppiumDriver } = require(path_1.default.join(appiumDir(), 'build/lib/appium.js'));
72
+ const umbrella = new AppiumDriver({});
73
+ umbrella.pluginClasses = new Map(plugins);
74
+ umbrella.driverConfig = {
75
+ findMatchingDriver: (caps) => __awaiter(this, void 0, void 0, function* () {
76
+ if (caps.automationName !== exports.FAKE_AUTOMATION) {
77
+ throw new Error(`Could not find a driver for automationName '${caps.automationName}' and platformName ` +
78
+ 'Android.');
79
+ }
80
+ return { driver: fakeInnerDriverClass(), version: '0.0.1', driverName: 'fake' };
81
+ }),
82
+ };
83
+ return umbrella;
84
+ }
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ /**
3
+ * An in-memory device table to stub `prisma.device` with, so a spec runs the
4
+ * real PrismaDeviceStore. It follows Prisma's rules: `undefined` leaves a
5
+ * column alone, `null` clears it, an upsert takes `create` for a new row and
6
+ * `update` for a known one, and `{ contains }` matches a substring (the store
7
+ * uses it for a host that isn't a URL).
8
+ */
9
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
10
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
11
+ return new (P || (P = Promise))(function (resolve, reject) {
12
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
13
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
14
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
15
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
16
+ });
17
+ };
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.fakeDeviceTable = fakeDeviceTable;
20
+ function fakeDeviceTable(seed) {
21
+ const rows = new Map();
22
+ const key = (udid, host) => `${udid}|${host}`;
23
+ const matches = (row, where) => {
24
+ if (!where)
25
+ return true;
26
+ return Object.entries(where).every(([field, cond]) => {
27
+ var _a;
28
+ if (field === 'OR')
29
+ return cond.some((w) => matches(row, w));
30
+ if (field === 'AND')
31
+ return cond.every((w) => matches(row, w));
32
+ if (cond && typeof cond === 'object' && 'in' in cond)
33
+ return cond.in.includes(row[field]);
34
+ if (cond && typeof cond === 'object' && 'contains' in cond)
35
+ return String((_a = row[field]) !== null && _a !== void 0 ? _a : '').includes(cond.contains);
36
+ return row[field] === cond;
37
+ });
38
+ };
39
+ const write = (row, data) => {
40
+ for (const [field, value] of Object.entries(data))
41
+ if (value !== undefined)
42
+ row[field] = value;
43
+ };
44
+ const defaults = () => ({
45
+ busy: false,
46
+ userBlocked: false,
47
+ offline: false,
48
+ session_id: null,
49
+ teamId: null,
50
+ sessionProgress: '',
51
+ sessionStartTime: 0,
52
+ totalUtilizationTimeMilliSec: 0,
53
+ claimSessionId: null,
54
+ claimedAt: null,
55
+ nodeBusy: false,
56
+ });
57
+ for (const r of seed)
58
+ rows.set(key(r.udid, r.host), Object.assign(Object.assign({}, defaults()), r));
59
+ return {
60
+ row: (udid) => [...rows.values()].find((r) => r.udid === udid),
61
+ delegate: {
62
+ findMany: (...args_1) => __awaiter(this, [...args_1], void 0, function* (args = {}) { return [...rows.values()].filter((r) => matches(r, args.where)).map((r) => (Object.assign({}, r))); }),
63
+ findUnique: (_a) => __awaiter(this, [_a], void 0, function* ({ where }) {
64
+ const r = rows.get(key(where.udid_host.udid, where.udid_host.host));
65
+ return r ? Object.assign({}, r) : null;
66
+ }),
67
+ upsert: (_a) => __awaiter(this, [_a], void 0, function* ({ where, create, update, }) {
68
+ const k = key(where.udid_host.udid, where.udid_host.host);
69
+ const found = rows.get(k);
70
+ if (found) {
71
+ write(found, update);
72
+ return Object.assign({}, found);
73
+ }
74
+ const created = defaults();
75
+ write(created, create);
76
+ rows.set(k, created);
77
+ return Object.assign({}, created);
78
+ }),
79
+ updateMany: (_a) => __awaiter(this, [_a], void 0, function* ({ where, data }) {
80
+ let count = 0;
81
+ for (const r of rows.values()) {
82
+ if (matches(r, where)) {
83
+ write(r, data);
84
+ count++;
85
+ }
86
+ }
87
+ return { count };
88
+ }),
89
+ deleteMany: (_a) => __awaiter(this, [_a], void 0, function* ({ where }) {
90
+ for (const [k, r] of rows)
91
+ if (matches(r, where))
92
+ rows.delete(k);
93
+ return { count: 0 };
94
+ }),
95
+ },
96
+ };
97
+ }
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useLokiStores = useLokiStores;
4
+ const device_store_1 = require("../../src/data-service/device-store");
5
+ // DeviceStoreFactory's cached stores, one per kind.
6
+ const CACHES = ['_deviceStore', '_pendingSessionStore', '_cliArgsStore', '_healEtalonStore'];
7
+ /**
8
+ * The in-memory (Loki) stores for the enclosing `describe`, however the file
9
+ * was started.
10
+ *
11
+ * DeviceStoreFactory picks Loki only while NODE_ENV is 'test'. `npm run
12
+ * test:all` sets it; `npx mocha <file>` doesn't, so a spec run on its own got
13
+ * the Prisma stores, which read and write the developer's
14
+ * ~/.cache/xenon/xenon.db. This holds NODE_ENV at 'test' and hands the
15
+ * factory new stores for the suite, then puts both back.
16
+ *
17
+ * Call it inside a `describe`, before the hooks that use a store, never at the
18
+ * top of a file.
19
+ */
20
+ function useLokiStores() {
21
+ const factory = device_store_1.DeviceStoreFactory;
22
+ const saved = {};
23
+ let previousEnv;
24
+ before(() => {
25
+ previousEnv = process.env.NODE_ENV;
26
+ process.env.NODE_ENV = 'test';
27
+ for (const key of CACHES) {
28
+ saved[key] = factory[key];
29
+ factory[key] = undefined;
30
+ }
31
+ });
32
+ after(() => {
33
+ for (const key of CACHES)
34
+ factory[key] = saved[key];
35
+ // Assigning undefined would store the string 'undefined'.
36
+ if (previousEnv === undefined)
37
+ delete process.env.NODE_ENV;
38
+ else
39
+ process.env.NODE_ENV = previousEnv;
40
+ });
41
+ }
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const http_1 = __importDefault(require("http"));
7
+ const supertest_1 = __importDefault(require("supertest"));
8
+ /**
9
+ * supertest's `request(app)`, served on 127.0.0.1. Import it in place of
10
+ * supertest's default export; everything else about a request is unchanged.
11
+ *
12
+ * supertest listens on every address (`app.listen(0)`) and then connects to
13
+ * 127.0.0.1. On macOS the kernel can give that listener a port another socket
14
+ * still holds on 127.0.0.1, and a connection to 127.0.0.1 goes to that socket.
15
+ * Another suite running at the same time then answers the request: a stray
16
+ * 501 or a socket hang-up. Listening on 127.0.0.1 ourselves rules that out,
17
+ * because the kernel never hands out a port already held there.
18
+ * (loopbackServers() in loopbackServer.ts does the same for specs that manage
19
+ * their servers themselves.)
20
+ *
21
+ * Binding 127.0.0.1 is asynchronous and supertest picks its URL in the
22
+ * constructor, so the listen waits until the request is sent (`end()`, which
23
+ * `await` and `.then()` call). A URL, or a server that is already listening,
24
+ * is used as supertest would.
25
+ */
26
+ const { Test } = supertest_1.default;
27
+ // One listen per server, however many requests are sent to it at once. The
28
+ // request that started it closes it, as supertest does.
29
+ const listens = new WeakMap();
30
+ function listenOnLoopback(server) {
31
+ const pending = listens.get(server);
32
+ if (pending)
33
+ return { port: pending, owner: false };
34
+ const port = new Promise((resolve, reject) => {
35
+ server.once('error', reject);
36
+ server.listen(0, '127.0.0.1', () => {
37
+ server.removeListener('error', reject);
38
+ resolve(server.address().port);
39
+ });
40
+ });
41
+ listens.set(server, port);
42
+ server.once('close', () => listens.delete(server));
43
+ return { port, owner: true };
44
+ }
45
+ class LoopbackTest extends Test {
46
+ serverAddress(app, path) {
47
+ if (app.address())
48
+ return super.serverAddress(app, path);
49
+ this.loopbackApp = app;
50
+ this.loopbackPath = path;
51
+ return `http://127.0.0.1${path}`; // replaced before the request is sent
52
+ }
53
+ end(fn) {
54
+ const app = this.loopbackApp;
55
+ if (!app)
56
+ return super.end(fn);
57
+ const { port, owner } = listenOnLoopback(app);
58
+ port.then((p) => {
59
+ var _a;
60
+ if (owner)
61
+ this._server = app;
62
+ this.url = `http://127.0.0.1:${p}${(_a = this.loopbackPath) !== null && _a !== void 0 ? _a : ''}`;
63
+ super.end(fn);
64
+ }, (err) => fn === null || fn === void 0 ? void 0 : fn(err, undefined));
65
+ return this;
66
+ }
67
+ }
68
+ const METHODS = http_1.default.METHODS.map((m) => m.toLowerCase());
69
+ function request(app) {
70
+ const obj = {};
71
+ for (const method of METHODS) {
72
+ obj[method] = (path) => new LoopbackTest(app, method, path);
73
+ }
74
+ obj.del = obj.delete;
75
+ return obj;
76
+ }
77
+ exports.default = request;