@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
@@ -52,6 +52,7 @@ const semver = __importStar(require("semver"));
52
52
  const logger_1 = __importDefault(require("../logger"));
53
53
  const deviceColumns_1 = require("./deviceColumns");
54
54
  const deviceFieldOwners_1 = require("./deviceFieldOwners");
55
+ const deviceClaims_1 = require("./deviceClaims");
55
56
  /** Logged once per key, so a chatty node doesn't flood the log. */
56
57
  const droppedDeviceKeys = new Set();
57
58
  class PrismaDeviceStore {
@@ -75,8 +76,8 @@ class PrismaDeviceStore {
75
76
  }
76
77
  }
77
78
  toIDevice(device) {
78
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
79
- return Object.assign(Object.assign({}, device), { cloud: this.safeParse(device.cloud, 'cloud', device.udid), capability: this.safeParse(device.capability, 'capability', device.udid), chromeDriverPath: this.safeParse(device.chromeDriverPath, 'chromeDriverPath', device.udid), tags: this.safeParse(device.tags, 'tags', device.udid), platform: (device.platform || 'android'), name: device.name || 'unknown', state: device.state || 'available', sdk: device.sdk || 'unknown', deviceType: device.deviceType || 'real', busy: (_a = device.busy) !== null && _a !== void 0 ? _a : false, userBlocked: (_b = device.userBlocked) !== null && _b !== void 0 ? _b : false, realDevice: (_c = device.realDevice) !== null && _c !== void 0 ? _c : true, lastHealthCheckAt: (_d = device.lastHealthCheckAt) !== null && _d !== void 0 ? _d : undefined, healthStatus: (_e = device.healthStatus) !== null && _e !== void 0 ? _e : 'Healthy', healthCheckError: (_f = device.healthCheckError) !== null && _f !== void 0 ? _f : undefined, batteryLevel: (_g = device.batteryLevel) !== null && _g !== void 0 ? _g : undefined, thermalStatus: (_h = device.thermalStatus) !== null && _h !== void 0 ? _h : undefined, storageFree: (_j = device.storageFree) !== null && _j !== void 0 ? _j : undefined, sessionProgress: (_k = device.sessionProgress) !== null && _k !== void 0 ? _k : '', totalHealedCount: (_l = device.totalHealedCount) !== null && _l !== void 0 ? _l : 0 });
79
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
80
+ return Object.assign(Object.assign({}, device), { cloud: this.safeParse(device.cloud, 'cloud', device.udid), capability: this.safeParse(device.capability, 'capability', device.udid), chromeDriverPath: this.safeParse(device.chromeDriverPath, 'chromeDriverPath', device.udid), tags: this.safeParse(device.tags, 'tags', device.udid), platform: (device.platform || 'android'), name: device.name || 'unknown', state: device.state || 'available', sdk: device.sdk || 'unknown', deviceType: device.deviceType || 'real', busy: (_a = device.busy) !== null && _a !== void 0 ? _a : false, userBlocked: (_b = device.userBlocked) !== null && _b !== void 0 ? _b : false, realDevice: (_c = device.realDevice) !== null && _c !== void 0 ? _c : true, lastHealthCheckAt: (_d = device.lastHealthCheckAt) !== null && _d !== void 0 ? _d : undefined, healthStatus: (_e = device.healthStatus) !== null && _e !== void 0 ? _e : 'Healthy', healthCheckError: (_f = device.healthCheckError) !== null && _f !== void 0 ? _f : undefined, batteryLevel: (_g = device.batteryLevel) !== null && _g !== void 0 ? _g : undefined, thermalStatus: (_h = device.thermalStatus) !== null && _h !== void 0 ? _h : undefined, storageFree: (_j = device.storageFree) !== null && _j !== void 0 ? _j : undefined, sessionProgress: (_k = device.sessionProgress) !== null && _k !== void 0 ? _k : '', totalHealedCount: (_l = device.totalHealedCount) !== null && _l !== void 0 ? _l : 0, nodeBusy: (_m = device.nodeBusy) !== null && _m !== void 0 ? _m : false });
80
81
  }
81
82
  fromIDevice(device) {
82
83
  const data = Object.assign({}, device);
@@ -208,17 +209,12 @@ class PrismaDeviceStore {
208
209
  logger_1.default.debug(`[PrismaStore] Update device ${udid} at ${host}: ${result.count} records affected`);
209
210
  });
210
211
  }
211
- updateDevices(filter, updateFn) {
212
+ touchSession(sessionId, at) {
212
213
  return __awaiter(this, void 0, void 0, function* () {
213
- const devices = yield this.prisma.device.findMany({ where: filter });
214
- for (const d of devices) {
215
- const idv = this.toIDevice(d);
216
- updateFn(idv);
217
- yield this.prisma.device.update({
218
- where: { udid_host: { udid: d.udid, host: d.host } },
219
- data: this.fromIDevice(idv),
220
- });
221
- }
214
+ yield this.prisma.device.updateMany({
215
+ where: { session_id: sessionId },
216
+ data: { lastCmdExecutedAt: at },
217
+ });
222
218
  });
223
219
  }
224
220
  addDevices(devices_1) {
@@ -232,31 +228,63 @@ class PrismaDeviceStore {
232
228
  const known = new Set(existing.map((d) => `${d.udid}@${d.host}`));
233
229
  const added = [];
234
230
  for (const device of devices) {
235
- const data = this.fromIDevice(device);
236
- // Use upsert to avoid race conditions and unique constraint errors. A
237
- // phone already here gets only its discovery columns (AddDevicesOptions).
238
- const d = yield this.prisma.device.upsert({
239
- where: { udid_host: { udid: device.udid, host: device.host } },
240
- update: options.mirror ? data : (0, deviceFieldOwners_1.pickDiscoveryFields)(data),
241
- create: data,
242
- });
231
+ const d = options.nodeReport
232
+ ? yield this.addNodeReport(device)
233
+ : yield this.prisma.device.upsert({
234
+ // Use upsert to avoid race conditions and unique constraint errors. A
235
+ // phone already here gets only its discovery columns (AddDevicesOptions).
236
+ where: { udid_host: { udid: device.udid, host: device.host } },
237
+ update: (0, deviceFieldOwners_1.pickDiscoveryFields)(this.fromIDevice(device)),
238
+ create: this.fromIDevice(device),
239
+ });
243
240
  if (!known.has(`${device.udid}@${device.host}`))
244
241
  added.push(this.toIDevice(d));
245
242
  }
246
243
  return added;
247
244
  });
248
245
  }
249
- removeDevices(filter) {
246
+ /**
247
+ * A node's report of one phone (AddDevicesOptions.nodeReport). The node's
248
+ * `busy` goes to `nodeBusy`. Busy there makes the phone busy here; free
249
+ * there frees it only where this hub holds no claim, in one conditional
250
+ * update, so a report the node sent before the hub claimed the phone can't
251
+ * undo the claim.
252
+ */
253
+ addNodeReport(device) {
250
254
  return __awaiter(this, void 0, void 0, function* () {
255
+ const { udid, host } = device;
256
+ const nodeBusy = device.busy === true;
257
+ const reported = Object.assign(Object.assign({}, (0, deviceFieldOwners_1.pickNodeReportFields)(this.fromIDevice(device))), { nodeBusy });
258
+ const row = yield this.prisma.device.upsert({
259
+ where: { udid_host: { udid, host } },
260
+ update: nodeBusy ? Object.assign(Object.assign({}, reported), { busy: true }) : reported,
261
+ create: Object.assign(Object.assign({}, reported), { udid, host, busy: nodeBusy }),
262
+ });
263
+ if (nodeBusy)
264
+ return row;
265
+ // Nothing of the hub's may hold it either: a claim, or a hold such as a
266
+ // preview or a recording (session_id), exactly as a release checks.
267
+ yield this.prisma.device.updateMany({
268
+ where: Object.assign({ udid, host, busy: true }, deviceClaims_1.UNHELD),
269
+ data: { busy: false },
270
+ });
271
+ return row;
272
+ });
273
+ }
274
+ removeDevices(filter_1) {
275
+ return __awaiter(this, arguments, void 0, function* (filter, options = {}) {
251
276
  // Principal Fix: Support partial host matching (e.g., "192.168.0.100" should match "http://192.168.0.100:4723")
252
277
  // This is necessary because the device tracker passes the raw IP, not the full URL.
278
+ // `exactHost` turns it off, for a caller that names rows by their own host.
253
279
  const whereClause = {};
254
280
  if (filter.udid) {
255
281
  whereClause.udid = filter.udid;
256
282
  }
257
283
  if (filter.host) {
258
284
  // Use Prisma's `contains` for partial matching if host doesn't look like a full URL
259
- if (filter.host.startsWith('http://') || filter.host.startsWith('https://')) {
285
+ if (options.exactHost ||
286
+ filter.host.startsWith('http://') ||
287
+ filter.host.startsWith('https://')) {
260
288
  whereClause.host = filter.host;
261
289
  }
262
290
  else {
@@ -266,9 +294,9 @@ class PrismaDeviceStore {
266
294
  yield this.prisma.device.deleteMany({ where: whereClause });
267
295
  });
268
296
  }
269
- clearStorage() {
297
+ clearStorage(onlyHosts) {
270
298
  return __awaiter(this, void 0, void 0, function* () {
271
- yield this.prisma.device.deleteMany();
299
+ yield this.prisma.device.deleteMany(onlyHosts ? { where: { host: { in: [...onlyHosts] } } } : undefined);
272
300
  });
273
301
  }
274
302
  findDevice(filter) {
@@ -283,8 +311,8 @@ class PrismaDeviceStore {
283
311
  return devices.map((d) => this.toIDevice(d));
284
312
  });
285
313
  }
286
- findAndLockDevice(filterOptions) {
287
- return __awaiter(this, void 0, void 0, function* () {
314
+ findAndLockDevice(filterOptions_1) {
315
+ return __awaiter(this, arguments, void 0, function* (filterOptions, options = {}) {
288
316
  // Phase 2: exclude devices held by an active lease.
289
317
  const activeLeases = yield this.prisma.lease.findMany({
290
318
  where: { status: 'active' },
@@ -298,6 +326,7 @@ class PrismaDeviceStore {
298
326
  if (blockedKeys.has(`${device.udid}@${device.host}`)) {
299
327
  continue;
300
328
  }
329
+ const now = Date.now();
301
330
  const result = yield this.prisma.device.updateMany({
302
331
  where: {
303
332
  udid: device.udid,
@@ -305,11 +334,7 @@ class PrismaDeviceStore {
305
334
  busy: false, // Double check it's still free
306
335
  offline: false,
307
336
  },
308
- data: {
309
- busy: true,
310
- lastCmdExecutedAt: Date.now(),
311
- sessionStartTime: Date.now(),
312
- },
337
+ data: Object.assign({ busy: true, lastCmdExecutedAt: now, sessionStartTime: now }, (options.claim ? { claimedAt: now, claimSessionId: null } : {})),
313
338
  });
314
339
  if (result.count === 1) {
315
340
  logger_1.default.info(`[PrismaStore] Successfully locked device ${device.udid} atomically`);
@@ -323,6 +348,52 @@ class PrismaDeviceStore {
323
348
  return null;
324
349
  });
325
350
  }
351
+ claimForSession(udid, host, claimedAt, sessionId, update) {
352
+ return __awaiter(this, void 0, void 0, function* () {
353
+ // The pending claim this phone was allocated with, or none left at all
354
+ // (released while the session was created, and not taken since).
355
+ const ours = claimedAt == null ? [{ claimedAt: null }] : [{ claimedAt }, { claimedAt: null }];
356
+ const result = yield this.prisma.device.updateMany({
357
+ where: { udid, host, claimSessionId: null, OR: ours },
358
+ data: Object.assign(Object.assign({}, this.fromIDevice(update)), { busy: true, claimSessionId: sessionId, claimedAt: claimedAt !== null && claimedAt !== void 0 ? claimedAt : Date.now() }),
359
+ });
360
+ return result.count > 0;
361
+ });
362
+ }
363
+ releaseClaim(udid, host, ref, update) {
364
+ return __awaiter(this, void 0, void 0, function* () {
365
+ const ended = yield this.prisma.device.updateMany({
366
+ where: Object.assign({ udid, host }, (0, deviceClaims_1.claimWhere)(ref)),
367
+ data: Object.assign(Object.assign({}, this.fromIDevice(update)), deviceClaims_1.CLAIM_RESET),
368
+ });
369
+ if (ended.count === 0)
370
+ return false;
371
+ // Not `busy: false` with the step above: a node reporting the phone busy,
372
+ // or a hold taken since, keeps it busy.
373
+ yield this.prisma.device.updateMany({
374
+ where: Object.assign({ udid, host, busy: true }, deviceClaims_1.UNHELD),
375
+ data: { busy: false },
376
+ });
377
+ return true;
378
+ });
379
+ }
380
+ releaseLeaseLock(udid, host) {
381
+ return __awaiter(this, void 0, void 0, function* () {
382
+ const cleared = yield this.prisma.device.updateMany({
383
+ where: Object.assign({ udid, host, busy: true }, deviceClaims_1.UNHELD),
384
+ data: { busy: false },
385
+ });
386
+ return cleared.count > 0;
387
+ });
388
+ }
389
+ markNodeBusy(udid, host) {
390
+ return __awaiter(this, void 0, void 0, function* () {
391
+ yield this.prisma.device.updateMany({
392
+ where: { udid, host },
393
+ data: { nodeBusy: true, busy: true },
394
+ });
395
+ });
396
+ }
326
397
  resetMetrics() {
327
398
  return __awaiter(this, void 0, void 0, function* () {
328
399
  yield this.prisma.device.updateMany({
@@ -54,6 +54,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
54
54
  return (mod && mod.__esModule) ? mod : { "default": mod };
55
55
  };
56
56
  Object.defineProperty(exports, "__esModule", { value: true });
57
+ exports.includesAndroidDevice = includesAndroidDevice;
57
58
  const displayState_1 = require("./android/displayState");
58
59
  const networkAddresses_1 = require("../helpers/networkAddresses");
59
60
  const appium_adb_1 = require("appium-adb");
@@ -77,8 +78,24 @@ const Devices_1 = __importDefault(require("./cloud/Devices"));
77
78
  const DeviceLockManager_1 = require("./android/DeviceLockManager");
78
79
  const androidIdentity_1 = require("./android/androidIdentity");
79
80
  const deviceIdentity_1 = require("./deviceIdentity");
81
+ const localDeviceHosts_1 = require("./localDeviceHosts");
80
82
  const PluginContext_1 = require("../PluginContext");
81
83
  const typedi_2 = require("typedi");
84
+ /**
85
+ * Whether a server set to `androidDeviceType` serves this phone: real phones,
86
+ * emulators ('simulated'), or both, and with `bootedEmulators` only the
87
+ * emulators that are up. Discovery and a plugged phone (onDeviceAdded) both
88
+ * ask this, so a phone the server excludes is never added by either.
89
+ */
90
+ function includesAndroidDevice(device, androidDeviceType, bootedEmulators) {
91
+ if (androidDeviceType === 'real')
92
+ return device.deviceType === 'real';
93
+ if (androidDeviceType === 'simulated' && device.deviceType !== 'emulator')
94
+ return false;
95
+ if (bootedEmulators && device.deviceType === 'emulator')
96
+ return device.state === 'device';
97
+ return true;
98
+ }
82
99
  let AndroidDeviceManager = class AndroidDeviceManager {
83
100
  constructor(context) {
84
101
  this.context = context;
@@ -148,33 +165,7 @@ let AndroidDeviceManager = class AndroidDeviceManager {
148
165
  devices = yield this.fetchAndroidDevices(existingDeviceDetails, this.pluginArgs);
149
166
  logger_1.default.info(`Found ${devices.length} android devices`);
150
167
  }
151
- if (deviceTypes.androidDeviceType === 'real') {
152
- return devices.filter((device) => {
153
- console.log(`Filtering device ${device.udid}, type: ${device.deviceType}, expected real`);
154
- return device.deviceType === 'real';
155
- });
156
- }
157
- else if (deviceTypes.androidDeviceType === 'simulated') {
158
- const simulated = devices.filter((device) => {
159
- return device.deviceType === 'emulator';
160
- });
161
- if (this.pluginArgs.bootedEmulators) {
162
- return simulated.filter((device) => device.state === 'device');
163
- }
164
- return simulated;
165
- // return both real and simulated (emulated) devices
166
- }
167
- else {
168
- if (this.pluginArgs.bootedEmulators) {
169
- return devices.filter((device) => {
170
- if (device.deviceType === 'emulator') {
171
- return device.state === 'device';
172
- }
173
- return true;
174
- });
175
- }
176
- return devices;
177
- }
168
+ return devices.filter((device) => includesAndroidDevice(device, deviceTypes.androidDeviceType, this.pluginArgs.bootedEmulators));
178
169
  }
179
170
  catch (e) {
180
171
  logger_1.default.error(`Error while getting android devices. Error: ${e instanceof Error ? e.message : e}`);
@@ -196,7 +187,11 @@ let AndroidDeviceManager = class AndroidDeviceManager {
196
187
  adbInstance.adbRemoteHost === null
197
188
  ? this.pluginArgs.bindHostOrIp
198
189
  : adbInstance.adbRemoteHost;
199
- const existingDevice = existingDeviceDetails.find((dev) => dev.udid === device.udid && dev.host.includes(this.pluginArgs.bindHostOrIp));
190
+ // This server's own row for the phone: the exact host deviceInfo
191
+ // would write. A node on this machine shares the IP, so its row
192
+ // for the same phone must not be taken for ours.
193
+ const ownHost = (0, localDeviceHosts_1.androidDeviceHost)(this.pluginArgs, this.hostPort, adbInstance);
194
+ const existingDevice = existingDeviceDetails.find((dev) => dev.udid === device.udid && dev.host === ownHost);
200
195
  if (existingDevice) {
201
196
  if (device.state === 'device') {
202
197
  logger_1.default.debug(`Android Device details for ${device.udid} already available and online`);
@@ -263,16 +258,7 @@ let AndroidDeviceManager = class AndroidDeviceManager {
263
258
  }
264
259
  // Optional: a device that can't say still registers, as before.
265
260
  const identity = yield this.getIdentity(adbInstance, device.udid, realDevice);
266
- let host;
267
- if (adbInstance.adbHost != null) {
268
- host = `http://${adbInstance.adbHost}:${adbInstance.adbPort}`;
269
- }
270
- else if (pluginArgs.remoteMachineProxyIP !== undefined) {
271
- host = `http://${pluginArgs.remoteMachineProxyIP}:${hostPort}`;
272
- }
273
- else {
274
- host = `http://${pluginArgs.bindHostOrIp}:${hostPort}`;
275
- }
261
+ const host = (0, localDeviceHosts_1.androidDeviceHost)(pluginArgs, hostPort, adbInstance);
276
262
  return Object.assign({ adbRemoteHost: (_a = adbInstance.adbRemoteHost) !== null && _a !== void 0 ? _a : undefined, adbPort: adbInstance.adbPort, systemPort, sdk: sdk !== null && sdk !== void 0 ? sdk : 'unknown', realDevice, name: name !== null && name !== void 0 ? name : 'unknown', busy: false, state: device.state, udid: device.udid, platform: 'android', deviceType: realDevice ? 'real' : 'emulator', host, totalUtilizationTimeMilliSec: totalUtilizationTimeMilliSec, sessionStartTime: 0, userBlocked: false, ip: (0, networkAddresses_1.sanitizeDeviceNetworkIp)(yield this.getDeviceIp(adbInstance, device.udid)), cpuArchitecture: yield this.getCpuArchitecture(adbInstance, device.udid) }, identity);
277
263
  });
278
264
  }
@@ -477,9 +463,7 @@ let AndroidDeviceManager = class AndroidDeviceManager {
477
463
  const adbRemote = pluginArgs.adbRemote;
478
464
  if (adbRemote !== undefined && adbRemote.length > 0) {
479
465
  const promises = adbRemote.map((value) => __awaiter(this, void 0, void 0, function* () {
480
- const adbRemoteValue = value.split(':');
481
- const adbHost = adbRemoteValue[0];
482
- const adbPort = parseInt(adbRemoteValue[1]) || 5037;
466
+ const { adbHost, adbPort } = (0, localDeviceHosts_1.parseAdbRemote)(value);
483
467
  const cloneAdb = originalADB.clone({
484
468
  remoteAdbHost: adbHost,
485
469
  adbPort,
@@ -520,6 +504,16 @@ let AndroidDeviceManager = class AndroidDeviceManager {
520
504
  return;
521
505
  }
522
506
  this.cancelAbort(newDevice.udid);
507
+ // Discovery's rule, before anything is leased or written: a phone of a
508
+ // kind this server doesn't serve was listed (and could be allocated)
509
+ // until the next sync pruned it. A phone that can't say goes on to
510
+ // deviceInfo, which needs the same property and skips it.
511
+ const realDevice = yield this.isRealDevice(originalADB, newDevice.udid).catch(() => undefined);
512
+ if (realDevice !== undefined &&
513
+ !includesAndroidDevice({ deviceType: realDevice ? 'real' : 'emulator', state: newDevice.state }, this.pluginArgs.androidDeviceType, this.pluginArgs.bootedEmulators)) {
514
+ logger_1.default.info(`Device ${newDevice.udid} is ${realDevice ? 'a real phone' : 'an emulator'}, and androidDeviceType is ${this.pluginArgs.androidDeviceType}. Ignoring`);
515
+ return;
516
+ }
523
517
  const trackedDevice = yield this.deviceInfo(newDevice, originalADB, this.pluginArgs, this.hostPort);
524
518
  if (!trackedDevice) {
525
519
  logger_1.default.info(`Cannot get device info for ${newDevice.udid}. Skipping`);
@@ -577,10 +571,14 @@ let AndroidDeviceManager = class AndroidDeviceManager {
577
571
  }
578
572
  onDeviceRemoved(device, pluginArgs) {
579
573
  return __awaiter(this, void 0, void 0, function* () {
574
+ // The host onDeviceAdded files the phone under (deviceInfo with the local
575
+ // adb). Exact: a bare IP is matched as a substring by the store and by
576
+ // the hub, and would also take another server's row with this udid.
580
577
  const clonedDevice = {
581
578
  udid: device['id'],
582
- host: pluginArgs.bindHostOrIp,
579
+ host: (0, localDeviceHosts_1.androidDeviceHost)(pluginArgs, this.hostPort),
583
580
  state: device.type,
581
+ nodeId: this.nodeId,
584
582
  };
585
583
  if (pluginArgs.hub != undefined) {
586
584
  const nodeDevices = new NodeDevices_1.default(pluginArgs.hub, {
@@ -63,6 +63,9 @@ const schedule = __importStar(require("node-schedule"));
63
63
  const web_config_service_1 = require("../data-service/web-config-service");
64
64
  const SessionManager_1 = require("../sessions/SessionManager");
65
65
  const EventBus_1 = require("../services/EventBus");
66
+ const deviceClaims_1 = require("../data-service/deviceClaims");
67
+ const PluginContext_1 = require("../PluginContext");
68
+ const localDeviceHosts_1 = require("./localDeviceHosts");
66
69
  let HealthMonitorService = class HealthMonitorService {
67
70
  constructor(webConfigService) {
68
71
  this.webConfigService = webConfigService;
@@ -145,14 +148,26 @@ let HealthMonitorService = class HealthMonitorService {
145
148
  const devices = yield store.getAllDevices();
146
149
  const manager = typedi_1.Container.get(index_1.XenonManager);
147
150
  const instances = yield manager.deviceInstances();
151
+ const context = typedi_1.Container.get(PluginContext_1.PluginContext);
152
+ const local = (0, localDeviceHosts_1.localDeviceHosts)(context.pluginArgs, context.port);
148
153
  for (const device of devices) {
149
- // Only check devices managed by this node
150
- if (device.cloud)
154
+ // Only this server's own phones. A hub keeps its nodes' phones in the
155
+ // same table; its adb can't reach them, so each came back unhealthy,
156
+ // was written over the node's report, "recovered" and, when busy with
157
+ // a session not in this hub's memory, reclaimed. Their node checks them.
158
+ if (device.cloud || !(0, localDeviceHosts_1.isOwnDevice)(local, context.nodeId, device))
151
159
  continue;
152
160
  // Principal Intelligence: Health monitor must detect "Zombie Busy" devices.
153
161
  // A device is a Zombie if it's marked busy in DB but NOT found in Hub's session map
154
162
  // AND not currently streaming to a manual viewer.
155
163
  if (device.busy) {
164
+ // Not a zombie: a session still being created on it (its session is
165
+ // in no map yet; the claim has its own timeout), or, on a hub, a
166
+ // node reporting it busy there (deviceClaims.ts).
167
+ if (((0, deviceClaims_1.isPendingClaim)(device) && !(0, deviceClaims_1.pendingClaimExpired)(device, Date.now())) ||
168
+ device.nodeBusy) {
169
+ continue;
170
+ }
156
171
  const hasActiveSession = SessionManager_1.SESSION_MANAGER.isValidSession(device.session_id || '');
157
172
  const isManualStream = (_a = device.session_id) === null || _a === void 0 ? void 0 : _a.startsWith('manual_');
158
173
  // NEW: Check for active manual streams (dashboard viewers)
@@ -99,7 +99,11 @@ let IOSDeviceManager = class IOSDeviceManager {
99
99
  result.screenHeight = String(streamStatus.screenHeight);
100
100
  }
101
101
  else {
102
- const storeDevice = yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid: device.udid });
102
+ // This phone's own row: another server's for the same udid is not it.
103
+ const storeDevice = yield device_store_1.DeviceStoreFactory.getStore().findDevice({
104
+ udid: device.udid,
105
+ host: device.host,
106
+ });
103
107
  if (storeDevice === null || storeDevice === void 0 ? void 0 : storeDevice.screenWidth) {
104
108
  result.screenWidth = storeDevice.screenWidth;
105
109
  result.screenHeight = storeDevice.screenHeight;
@@ -67,13 +67,18 @@ class NodeDevices {
67
67
  }
68
68
  });
69
69
  }
70
- unRegisterNode(host) {
70
+ /**
71
+ * Ask the hub to forget this node's phones filed under `host`. With the
72
+ * node's id the hub takes every phone this node reported, whatever its
73
+ * host; it never takes another server's (grid.ts, removeNodeDevices).
74
+ */
75
+ unRegisterNode(host, nodeId) {
71
76
  return __awaiter(this, void 0, void 0, function* () {
72
77
  logger_1.default.info(`Unregistering node ${this.host}/xenon/api/register`);
73
78
  try {
74
79
  const client = InternalHttpClient_1.InternalHttpClient.getClient(this.tlsRejectUnauthorized);
75
80
  yield client.post(`${this.host}/xenon/api/register`, [], {
76
- params: { type: 'unregister', host },
81
+ params: Object.assign({ type: 'unregister', host }, (nodeId ? { nodeId } : {})),
77
82
  headers: this.nodeHeaders(),
78
83
  });
79
84
  logger_1.default.info(`Unregistered node ${host} from hub`);
@@ -57,7 +57,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
57
57
  const child_process_1 = require("child_process");
58
58
  const typedi_1 = require("typedi");
59
59
  const logger_1 = __importDefault(require("../../logger"));
60
- const device_store_1 = require("../../data-service/device-store");
60
+ const ownDeviceRow_1 = require("../ownDeviceRow");
61
61
  const H264Multiplexer_1 = require("./H264Multiplexer");
62
62
  const h264NalParser_1 = require("./h264NalParser");
63
63
  const androidH264Config_1 = require("../../app/routers/androidH264Config");
@@ -91,6 +91,8 @@ let AndroidH264StreamService = class AndroidH264StreamService {
91
91
  this.startWatchdog();
92
92
  }
93
93
  startWatchdog() {
94
+ // unref()ed below: the server's listener keeps the process alive; this only
95
+ // needs to fire while it runs.
94
96
  setInterval(() => {
95
97
  const now = Date.now();
96
98
  for (const [udid, s] of this.sessions.entries()) {
@@ -111,7 +113,7 @@ let AndroidH264StreamService = class AndroidH264StreamService {
111
113
  void this.stop(udid).then(() => (0, previewHold_1.releaseIdlePreviewHold)(udid));
112
114
  }
113
115
  }
114
- }, 60000);
116
+ }, 60000).unref();
115
117
  }
116
118
  getMultiplexer(udid) {
117
119
  var _a;
@@ -150,7 +152,13 @@ let AndroidH264StreamService = class AndroidH264StreamService {
150
152
  session.capture = yield this.openCapture(udid, onPacket, source);
151
153
  // Give the first keyframe/config a moment so callers get a ready stream,
152
154
  // but never block start-up indefinitely.
153
- yield Promise.race([firstConfig, new Promise((r) => setTimeout(r, 3000))]);
155
+ let cap;
156
+ try {
157
+ yield Promise.race([firstConfig, new Promise((r) => (cap = setTimeout(r, 3000)))]);
158
+ }
159
+ finally {
160
+ clearTimeout(cap);
161
+ }
154
162
  return mux;
155
163
  }
156
164
  catch (e) {
@@ -266,7 +274,7 @@ let AndroidH264StreamService = class AndroidH264StreamService {
266
274
  startScrcpyOrThrow(udid, onPacket) {
267
275
  return __awaiter(this, void 0, void 0, function* () {
268
276
  const { ScrcpyServerSession, scrcpyMaxSizeFromDims } = yield Promise.resolve().then(() => __importStar(require('./ScrcpyServerSession')));
269
- const device = yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid });
277
+ const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
270
278
  const maxSize = scrcpyMaxSizeFromDims(Number(device === null || device === void 0 ? void 0 : device.screenWidth), Number(device === null || device === void 0 ? void 0 : device.screenHeight));
271
279
  const parser = new h264NalParser_1.H264NalParser();
272
280
  const session = new ScrcpyServerSession(udid);
@@ -405,7 +413,7 @@ let AndroidH264StreamService = class AndroidH264StreamService {
405
413
  resolveCaptureSize(udid) {
406
414
  return __awaiter(this, void 0, void 0, function* () {
407
415
  try {
408
- const device = yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid });
416
+ const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
409
417
  const sw = Number(device === null || device === void 0 ? void 0 : device.screenWidth);
410
418
  const sh = Number(device === null || device === void 0 ? void 0 : device.screenHeight);
411
419
  if (Number.isFinite(sw) && Number.isFinite(sh) && sw > 0 && sh > 0) {
@@ -59,6 +59,7 @@ const http_1 = __importDefault(require("http"));
59
59
  const sharp_1 = __importDefault(require("sharp"));
60
60
  const logger_1 = __importDefault(require("../../logger"));
61
61
  const device_store_1 = require("../../data-service/device-store");
62
+ const ownDeviceRow_1 = require("../ownDeviceRow");
62
63
  const DeviceLockManager_1 = require("./DeviceLockManager");
63
64
  const typedi_1 = require("typedi");
64
65
  const ResourceIsolationService_1 = require("../../services/ResourceIsolationService");
@@ -80,6 +81,8 @@ let AndroidStreamService = class AndroidStreamService {
80
81
  this.startWatchdog();
81
82
  }
82
83
  startWatchdog() {
84
+ // unref()ed below: the server's listener keeps the process alive; this only
85
+ // needs to fire while it runs.
83
86
  setInterval(() => __awaiter(this, void 0, void 0, function* () {
84
87
  const now = Date.now();
85
88
  for (const [udid, session] of this.sessions.entries()) {
@@ -92,7 +95,7 @@ let AndroidStreamService = class AndroidStreamService {
92
95
  }
93
96
  // Every minute, not every hour: an hourly check left an abandoned
94
97
  // preview hold in place for up to 70 minutes. The loop is in memory.
95
- }), 60000);
98
+ }), 60000).unref();
96
99
  }
97
100
  /**
98
101
  * Resolve the actual adb binary path (and remote-host args) via the same
@@ -272,7 +275,7 @@ let AndroidStreamService = class AndroidStreamService {
272
275
  `(${decision.reason}); discarding it and starting fresh.`);
273
276
  yield this.disposeSession(udid, existing);
274
277
  }
275
- const device = yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid });
278
+ const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
276
279
  if (!device)
277
280
  throw new Error(`Device ${udid} not found in DB`);
278
281
  // Resolve the real adb binary once, up front, so the capture loop never
@@ -315,7 +318,7 @@ let AndroidStreamService = class AndroidStreamService {
315
318
  catch (_d) {
316
319
  /* ignore */
317
320
  }
318
- yield portAllocator.release(mjpegPort).catch(() => undefined);
321
+ yield portAllocator.release(mjpegPort, udid).catch(() => undefined);
319
322
  yield portAllocator.blockPort(mjpegPort, 'mjpeg').catch(() => undefined);
320
323
  }
321
324
  }
@@ -346,7 +349,7 @@ let AndroidStreamService = class AndroidStreamService {
346
349
  logger_1.default.info(`[${udid}] MJPEG stream primed with first frame.`);
347
350
  }
348
351
  // Update device info in store to ensure other services (like VideoPipeline) can find the port
349
- const updatedDevice = yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid });
352
+ const updatedDevice = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
350
353
  if (updatedDevice) {
351
354
  yield device_store_1.DeviceStoreFactory.getStore().updateDevice(udid, updatedDevice.host, {
352
355
  mjpegServerPort: mjpegPort,
@@ -393,7 +396,7 @@ let AndroidStreamService = class AndroidStreamService {
393
396
  if (this.sessions.get(udid) === session)
394
397
  this.sessions.delete(udid);
395
398
  try {
396
- yield typedi_1.Container.get(PortAllocator_1.PortAllocator).release(session.mjpegPort);
399
+ yield typedi_1.Container.get(PortAllocator_1.PortAllocator).release(session.mjpegPort, udid);
397
400
  }
398
401
  catch (e) {
399
402
  logger_1.default.warn(`[${udid}] Failed to release mjpeg port lease while restarting stream: ${e}`);
@@ -420,7 +423,7 @@ let AndroidStreamService = class AndroidStreamService {
420
423
  // 'mjpeg' lease is released here — the device-level 'system' port (leased
421
424
  // by AndroidDeviceManager) belongs to the device lifecycle, not the stream.
422
425
  try {
423
- yield typedi_1.Container.get(PortAllocator_1.PortAllocator).release(session.mjpegPort);
426
+ yield typedi_1.Container.get(PortAllocator_1.PortAllocator).release(session.mjpegPort, udid);
424
427
  }
425
428
  catch (e) {
426
429
  logger_1.default.warn(`[${udid}] Failed to release mjpeg port lease on stop: ${e}`);
@@ -16,7 +16,7 @@ exports.mayReleasePreviewHold = mayReleasePreviewHold;
16
16
  exports.releaseIdlePreviewHold = releaseIdlePreviewHold;
17
17
  const typedi_1 = require("typedi");
18
18
  const logger_1 = __importDefault(require("../../logger"));
19
- const device_store_1 = require("../../data-service/device-store");
19
+ const ownDeviceRow_1 = require("../ownDeviceRow");
20
20
  const device_service_1 = require("../../data-service/device-service");
21
21
  const manualLock_1 = require("../../services/recording/manualLock");
22
22
  const recording_store_1 = require("../../services/recording/recording-store");
@@ -43,7 +43,7 @@ function releaseIdlePreviewHold(udid) {
43
43
  return __awaiter(this, void 0, void 0, function* () {
44
44
  var _a, _b, _c, _d, _e;
45
45
  try {
46
- const device = yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid });
46
+ const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
47
47
  if (!device)
48
48
  return false;
49
49
  // eslint-disable-next-line @typescript-eslint/no-var-requires