@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.
- package/README.md +2 -1
- package/lib/package.json +3 -4
- package/lib/public/assets/{AnnotationOverlay-BmTPCAds.js → AnnotationOverlay-XpewX5nn.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-B5eB7f8B.js → ApiKeyGate-BQDv3ZCC.js} +1 -1
- package/lib/public/assets/BugReportButton-C7OmS61B.js +10 -0
- package/lib/public/assets/{DeviceMosaicView-9DUoZnQi.js → DeviceMosaicView-wl3l2Zlt.js} +1 -1
- package/lib/public/assets/{EmptyState-lvyNiXev.js → EmptyState-fPxujqqE.js} +1 -1
- package/lib/public/assets/{FieldGroup-BduMPM9K.js → FieldGroup-5bCutWrs.js} +1 -1
- package/lib/public/assets/{FilterMenu-BSqX6I94.js → FilterMenu-owsBiDT1.js} +1 -1
- package/lib/public/assets/{Menu-Ba5cT7jZ.js → Menu-DFhqzPSt.js} +1 -1
- package/lib/public/assets/{Modal-CceoMY7D.js → Modal-Bw-a-E-e.js} +1 -1
- package/lib/public/assets/{RecordingPage-6T9dOidc.js → RecordingPage-WtByf5bC.js} +1 -1
- package/lib/public/assets/{RecordingsPage-qj6FdZXR.js → RecordingsPage-FpWWFNgT.js} +1 -1
- package/lib/public/assets/{SegmentedControl-CSRuZ8TR.js → SegmentedControl-Jzn9c7o8.js} +1 -1
- package/lib/public/assets/{SettingCard-Bp1kz4By.js → SettingCard-BR7nOhpT.js} +1 -1
- package/lib/public/assets/{Table-LV7zFyZg.js → Table-B_Va3V05.js} +1 -1
- package/lib/public/assets/{activity-D17TUloi.js → activity-DxRFWvkN.js} +1 -1
- package/lib/public/assets/{ai-settings-qvbhY89P.js → ai-settings-CQf-9_V6.js} +1 -1
- package/lib/public/assets/{api-keys-BfpvdG7G.js → api-keys-SoE1Kd7N.js} +1 -1
- package/lib/public/assets/{apps-Dbnzd0fW.js → apps-CgGXTlgn.js} +1 -1
- package/lib/public/assets/{arrow-left-DD1VjtC7.js → arrow-left-uQ6WIwiC.js} +1 -1
- package/lib/public/assets/{arrow-right-D0pnM3ma.js → arrow-right-C9RyfzXS.js} +1 -1
- package/lib/public/assets/{arrow-up-right-BIC8JOV6.js → arrow-up-right-DFHi_8yG.js} +1 -1
- package/lib/public/assets/{auth-shell-DWHPXSnI.js → auth-shell-Bii1t4qc.js} +2 -2
- package/lib/public/assets/{builds-page-ClCNBUv_.js → builds-page-Bnv8Z9hi.js} +1 -1
- package/lib/public/assets/{button-xtAhYR-f.js → button-_Y78wXiP.js} +1 -1
- package/lib/public/assets/{calendar-CupwJ7Bm.js → calendar-D4MqSOc2.js} +1 -1
- package/lib/public/assets/{check-CyuEoKkd.js → check-uGHCXe-L.js} +1 -1
- package/lib/public/assets/{chevron-left-DgX-F9xK.js → chevron-left-Dy1OqpzB.js} +1 -1
- package/lib/public/assets/{chevron-right-BKJgJnU9.js → chevron-right-BUsldEli.js} +1 -1
- package/lib/public/assets/{circle-check-CZdg8mMa.js → circle-check-D3Qq0tKp.js} +1 -1
- package/lib/public/assets/{circle-x-CZH6t9z-.js → circle-x-B4IOD1DE.js} +1 -1
- package/lib/public/assets/{clock-Cd-t7xpe.js → clock-BfYfzvSg.js} +1 -1
- package/lib/public/assets/{copy-C6zerX3I.js → copy-DZwYE5Va.js} +1 -1
- package/lib/public/assets/{device-explorer-B1pI6b_M.js → device-explorer-CRG4Utt5.js} +3 -3
- package/lib/public/assets/device-explorer-D2AqrrLN.css +1 -0
- package/lib/public/assets/{download-x9WP7mcL.js → download-CCDYo_TN.js} +1 -1
- package/lib/public/assets/{forgot-password-B0TEH4N-.js → forgot-password-DuLCmVFb.js} +1 -1
- package/lib/public/assets/{funnel-Cb40MfFN.js → funnel-CtKNPqzb.js} +1 -1
- package/lib/public/assets/{index-BR1wTIPC.js → index-3uxl3o7K.js} +43 -43
- package/lib/public/assets/{index-BvH_ekRy.js → index-Zy1Op5C1.js} +1 -1
- package/lib/public/assets/{input-DejcCiTI.js → input-CzTWvUXZ.js} +1 -1
- package/lib/public/assets/{keycodes-Mavx5iGg.js → keycodes-CI-nYDFM.js} +1 -1
- package/lib/public/assets/{layers-BKqYLiHE.js → layers-YZZGFyTX.js} +1 -1
- package/lib/public/assets/{lock-LFsFhc17.js → lock-L8tzWgDo.js} +1 -1
- package/lib/public/assets/{login-x2pkzt-t.js → login-DBbX-fu8.js} +1 -1
- package/lib/public/assets/{maintenance-settings-CYafcoV-.js → maintenance-settings-AJwkOIsE.js} +1 -1
- package/lib/public/assets/{monitor-_7_uJ8d9.js → monitor-Di_66F2m.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-BfLuURKN.js → mouse-pointer-2-Cnab2zdg.js} +1 -1
- package/lib/public/assets/{network-Z_zBhpsD.js → network-DiSumY-1.js} +1 -1
- package/lib/public/assets/{overview-BY5uv08S.js → overview-CwE4gXl5.js} +1 -1
- package/lib/public/assets/{page-header-CP2UkykG.js → page-header-CfR4Sm9V.js} +1 -1
- package/lib/public/assets/{plus-BTD0dnQ0.js → plus-DOS8P9CQ.js} +1 -1
- package/lib/public/assets/{profile-page-BhJZuCS7.js → profile-page-BYpHwQIH.js} +1 -1
- package/lib/public/assets/{reset-password-CMN0SO81.js → reset-password-BRVJU_qV.js} +1 -1
- package/lib/public/assets/{runbook-page-CBpPX9_5.js → runbook-page-Bi3JvAFZ.js} +1 -1
- package/lib/public/assets/{select-Uc9E5pye.js → select-Cqx7pEou.js} +1 -1
- package/lib/public/assets/{selector-detail-page-BSyBnRuU.js → selector-detail-page-C0DsncKj.js} +1 -1
- package/lib/public/assets/{selector-health-DzqxNkzm.js → selector-health-CAhpdilF.js} +1 -1
- package/lib/public/assets/{selector-health-page-CyPLjoD1.js → selector-health-page-CrCVeW2m.js} +1 -1
- package/lib/public/assets/{session-detail-page-BZQZN1P-.js → session-detail-page-F_hwYqEt.js} +1 -1
- package/lib/public/assets/{settings-CkSWiZq8.js → settings-C-s5pypL.js} +1 -1
- package/lib/public/assets/{status-pill-outline-BFYni2qo.js → status-pill-outline-DY5uzv6A.js} +1 -1
- package/lib/public/assets/{tablet-BSd-xSZK.js → tablet-Dli7gwyg.js} +1 -1
- package/lib/public/assets/{teams-PpoEQz31.js → teams-ui4IhHNC.js} +1 -1
- package/lib/public/assets/{trash-2-BorJv5_u.js → trash-2-BV4hSmXJ.js} +1 -1
- package/lib/public/assets/{upload-C3i7sAZl.js → upload-DkGy_J-L.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-CeZQ3EsH.js → use-password-reset-mode-BFtMqQOh.js} +1 -1
- package/lib/public/assets/{users-Drd7ifJx.js → users-BU0nXIVe.js} +1 -1
- package/lib/public/assets/{users-CEJ3fC78.js → users-Nb_rzRmF.js} +1 -1
- package/lib/public/assets/{video-off-CNzn6bPE.js → video-off-CUNzD2kH.js} +1 -1
- package/lib/public/assets/{webhook-settings-BFqX6QXV.js → webhook-settings-Ca3UFXZe.js} +1 -1
- package/lib/public/assets/{zap-4yN8Jdfw.js → zap-B2bH2vKX.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/InternalHttpClient.js +5 -1
- package/lib/src/XenonCapabilityManager.js +15 -1
- package/lib/src/app/appiumBasePath.js +32 -0
- package/lib/src/app/index.js +12 -0
- package/lib/src/app/insertBeforeRoutes.js +24 -4
- package/lib/src/app/registerCommandAuth.js +152 -33
- package/lib/src/app/routers/control.js +11 -104
- package/lib/src/app/routers/grid.js +28 -8
- package/lib/src/app/routers/nodePhoneControl.js +301 -0
- package/lib/src/app/ws/h264StreamWs.js +43 -12
- package/lib/src/app/ws/logcatWs.js +48 -14
- package/lib/src/app/ws/nodeSocketRelay.js +167 -0
- package/lib/src/app/ws/upgradeRouter.js +215 -0
- package/lib/src/dashboard/event-manager.js +7 -6
- package/lib/src/data-service/device-service.js +152 -49
- package/lib/src/data-service/device-store.js +92 -10
- package/lib/src/data-service/deviceClaims.js +116 -0
- package/lib/src/data-service/deviceFieldOwners.js +32 -10
- package/lib/src/data-service/prisma-store.js +102 -31
- package/lib/src/device-managers/AndroidDeviceManager.js +40 -42
- package/lib/src/device-managers/HealthMonitorService.js +17 -2
- package/lib/src/device-managers/IOSDeviceManager.js +5 -1
- package/lib/src/device-managers/NodeDevices.js +7 -2
- package/lib/src/device-managers/android/AndroidH264StreamService.js +13 -5
- package/lib/src/device-managers/android/AndroidStreamService.js +9 -6
- package/lib/src/device-managers/android/previewHold.js +2 -2
- package/lib/src/device-managers/ios/IOSDiscoveryService.js +19 -8
- package/lib/src/device-managers/ios/IOSStreamService.js +162 -40
- package/lib/src/device-managers/localDeviceHosts.js +86 -0
- package/lib/src/device-managers/ownDeviceRow.js +38 -0
- package/lib/src/device-utils.js +78 -24
- package/lib/src/gateway/createHandoff.js +73 -0
- package/lib/src/gateway/defaultGateway.js +80 -0
- package/lib/src/gateway/forwardToNode.js +147 -0
- package/lib/src/gateway/hubSessionToken.js +356 -0
- package/lib/src/gateway/internalCall.js +90 -0
- package/lib/src/gateway/nodeSessionStatus.js +140 -0
- package/lib/src/gateway/nodeWebDriverUrl.js +139 -0
- package/lib/src/gateway/sessionCreate.js +136 -0
- package/lib/src/gateway/sessionGateway.js +201 -0
- package/lib/src/gateway/sessionLocator.js +100 -0
- package/lib/src/generated/client/edge.js +7 -4
- package/lib/src/generated/client/index-browser.js +4 -1
- package/lib/src/generated/client/index.d.ts +112 -1
- package/lib/src/generated/client/index.js +7 -4
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +9 -0
- package/lib/src/generated/client/wasm.js +4 -1
- package/lib/src/helpers/index.js +5 -1
- package/lib/src/logger.js +5 -0
- package/lib/src/middleware/authMiddleware.js +80 -0
- package/lib/src/middleware/commandAuth.js +31 -17
- package/lib/src/middleware/csrfMiddleware.js +3 -2
- package/lib/src/middleware/deviceAccessGuard.js +4 -1
- package/lib/src/middleware/sessionListingFilter.js +153 -0
- package/lib/src/middleware/sessionUpgradeGuard.js +203 -0
- package/lib/src/plugin.js +27 -1
- package/lib/src/prisma.js +2 -1
- package/lib/src/services/DeviceReconciler.js +5 -2
- package/lib/src/services/OrphanSweeper.js +12 -19
- package/lib/src/services/PortAllocator.js +75 -16
- package/lib/src/services/ServerManager.js +63 -30
- package/lib/src/services/SessionLifecycleService.js +335 -76
- package/lib/src/services/ShutdownCoordinator.js +7 -1
- package/lib/src/services/SocketServer.js +11 -2
- package/lib/src/services/device-access/LiveSessionOwners.js +53 -0
- package/lib/src/services/device-access/SessionOwnerResolver.js +74 -0
- package/lib/src/services/device-access/ticketActorAccess.js +3 -1
- package/lib/src/services/lease/LeaseOrphanSweeper.js +34 -6
- package/lib/src/services/lease/LeaseService.js +19 -5
- package/lib/src/services/session/nodeCreateCaps.js +55 -0
- package/lib/src/services/session/sessionCredentials.js +7 -59
- package/lib/src/sessions/CloudSession.js +12 -0
- package/lib/src/sessions/LocalSession.js +52 -26
- package/lib/src/sessions/RemoteSession.js +116 -9
- package/lib/src/sessions/SessionManager.js +49 -6
- package/lib/src/sessions/appiumUmbrella.js +32 -0
- package/lib/test/helpers/appium-umbrella.js +84 -0
- package/lib/test/helpers/fake-device-table.js +97 -0
- package/lib/test/helpers/loki-stores.js +41 -0
- package/lib/test/helpers/loopbackRequest.js +77 -0
- package/lib/test/helpers/own-node-id.js +29 -0
- package/lib/test/helpers/scratch-database.js +100 -0
- package/lib/test/helpers/scratch-port-leases.js +119 -0
- package/lib/test/helpers/test-container.js +35 -27
- package/lib/test/integration/androidDevices.spec.js +5 -4
- package/lib/test/integration/ios/01iOSSimulator.spec.js +6 -5
- package/lib/test/integration/ios/02iOSDevices.spec.js +3 -2
- package/lib/test/integration/prisma-device-store-add-devices.spec.js +14 -4
- package/lib/test/setup/scratch-default-database.js +32 -0
- package/lib/test/unit/AndroidDeviceManager.spec.js +56 -1
- package/lib/test/unit/DeviceModel.spec.js +9 -9
- package/lib/test/unit/HealingCascade.test.js +5 -1
- package/lib/test/unit/OrphanSweeper.test.js +17 -13
- package/lib/test/unit/SessionHeartbeatWrite.test.js +6 -7
- package/lib/test/unit/StreamTicketService.spec.js +4 -1
- package/lib/test/unit/TokenEndpoint.spec.js +17 -3
- package/lib/test/unit/aggregate-hotspots-session-scope.spec.js +5 -5
- package/lib/test/unit/android-h264-service.spec.js +24 -0
- package/lib/test/unit/android-stream-stall-eof.spec.js +10 -0
- package/lib/test/unit/apps-team-visibility.spec.js +16 -16
- package/lib/test/unit/auth-me-auth-disabled.test.js +7 -4
- package/lib/test/unit/authMiddleware.appTicket.spec.js +5 -5
- package/lib/test/unit/authMiddleware.bearer.spec.js +4 -1
- package/lib/test/unit/authMiddleware.mcpAudience.spec.js +4 -1
- package/lib/test/unit/authMiddleware.streamTicket.spec.js +19 -7
- package/lib/test/unit/authMiddleware.test.js +3 -0
- package/lib/test/unit/bug-report/route.spec.js +9 -9
- package/lib/test/unit/cliargs-admin-only.spec.js +3 -3
- package/lib/test/unit/clipboard-route.spec.js +22 -8
- package/lib/test/unit/command-auth-appium-server.spec.js +187 -32
- package/lib/test/unit/command-auth.spec.js +31 -31
- package/lib/test/unit/control-device-lookup.spec.js +9 -9
- package/lib/test/unit/control-forward-decision.spec.js +115 -0
- package/lib/test/unit/control-lookup-failure.spec.js +3 -3
- package/lib/test/unit/create-error-message.spec.js +43 -0
- package/lib/test/unit/create-handoff.spec.js +86 -0
- package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
- package/lib/test/unit/device-access-guard.spec.js +66 -28
- package/lib/test/unit/device-service.spec.js +4 -0
- package/lib/test/unit/device-store-clear-at-boot.spec.js +48 -0
- package/lib/test/unit/device-sync-lost-update.spec.js +17 -89
- package/lib/test/unit/device-team-guard.spec.js +20 -20
- package/lib/test/unit/device-utils.spec.js +8 -0
- package/lib/test/unit/extract-team-cap.spec.js +65 -0
- package/lib/test/unit/grid-admin-only.spec.js +3 -3
- package/lib/test/unit/healing-tier-gate.spec.js +5 -1
- package/lib/test/unit/health-monitor-own-phones.spec.js +102 -0
- package/lib/test/unit/health-monitor-stream-lookup.spec.js +11 -1
- package/lib/test/unit/helpers.spec.js +27 -0
- package/lib/test/unit/hub-boot-sweep-recovery.spec.js +192 -0
- package/lib/test/unit/hub-create-once.spec.js +190 -0
- package/lib/test/unit/hub-create-session-umbrella.spec.js +160 -0
- package/lib/test/unit/hub-gateway-create-appium-server.spec.js +591 -0
- package/lib/test/unit/hub-node-control-forward.spec.js +608 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +594 -0
- package/lib/test/unit/hub-node-race.spec.js +426 -0
- package/lib/test/unit/hub-node-same-host.spec.js +356 -0
- package/lib/test/unit/hub-register-removal.spec.js +165 -0
- package/lib/test/unit/hub-remote-session-heartbeat.spec.js +277 -0
- package/lib/test/unit/hub-routed-session-recovery.spec.js +151 -0
- package/lib/test/unit/hub-session-token.spec.js +265 -0
- package/lib/test/unit/insert-before-routes.spec.js +5 -5
- package/lib/test/unit/install-repository-app-team.spec.js +6 -2
- package/lib/test/unit/interceptor/MitmProxyHost.spec.js +23 -10
- package/lib/test/unit/internal-call.spec.js +181 -0
- package/lib/test/unit/ios-discovery-same-host.spec.js +193 -0
- package/lib/test/unit/ios-stream-appium-tunnel.spec.js +317 -0
- package/lib/test/unit/ios-stream-port-leases.spec.js +299 -0
- package/lib/test/unit/ios-wda-status-identity.spec.js +84 -0
- package/lib/test/unit/is-xenon-running.spec.js +69 -0
- package/lib/test/unit/lease/LeaseOrphanSweeper.spec.js +2 -2
- package/lib/test/unit/lease/LeaseService.spec.js +28 -1
- package/lib/test/unit/lease/lease-reap-held-phone.spec.js +154 -0
- package/lib/test/unit/lease/lease-sweeper-expiry.spec.js +107 -0
- package/lib/test/unit/lease/lease-team-visibility.spec.js +2 -2
- package/lib/test/unit/lease/lease-token-log-filter.spec.js +242 -64
- package/lib/test/unit/lease/lease-token-oracle.spec.js +20 -7
- package/lib/test/unit/lease/session-lease-proof.spec.js +8 -3
- package/lib/test/unit/logcat-ws.spec.js +12 -1
- package/lib/test/unit/loopback-request.spec.js +81 -0
- package/lib/test/unit/mark-node-busy-loki.spec.js +53 -0
- package/lib/test/unit/node-create-caps.spec.js +54 -0
- package/lib/test/unit/node-gateway-create-appium-server.spec.js +354 -0
- package/lib/test/unit/node-hub-session-owner.spec.js +223 -0
- package/lib/test/unit/node-phone-control.spec.js +131 -0
- package/lib/test/unit/node-socket-relay.spec.js +357 -0
- package/lib/test/unit/omni-vision.spec.js +7 -3
- package/lib/test/unit/plugin.spec.js +45 -4
- package/lib/test/unit/port-allocator-os-probe.spec.js +151 -0
- package/lib/test/unit/profile-router.test.js +11 -11
- package/lib/test/unit/queue-requester.spec.js +5 -0
- package/lib/test/unit/queue-team-scope-routes.spec.js +17 -16
- package/lib/test/unit/queue-visibility.spec.js +1 -1
- package/lib/test/unit/recording-downloads-visibility.spec.js +2 -2
- package/lib/test/unit/recording-instrumentation.spec.js +5 -0
- package/lib/test/unit/recording-source-ended.spec.js +5 -0
- package/lib/test/unit/recording-store.spec.js +9 -3
- package/lib/test/unit/recordings-library-routes.spec.js +51 -51
- package/lib/test/unit/recordings-write-visibility.spec.js +2 -2
- package/lib/test/unit/remote-session-create.spec.js +130 -0
- package/lib/test/unit/reservation-team-visibility.spec.js +5 -5
- package/lib/test/unit/reset-link.test.js +14 -14
- package/lib/test/unit/session-app-resolution.spec.js +14 -10
- package/lib/test/unit/session-assets.spec.js +9 -9
- package/lib/test/unit/session-attribution-wiring.spec.js +29 -2
- package/lib/test/unit/session-create-layer.spec.js +255 -0
- package/lib/test/unit/session-credential-strip.spec.js +25 -9
- package/lib/test/unit/session-credentials.spec.js +0 -47
- package/lib/test/unit/session-listing-auth.spec.js +345 -0
- package/lib/test/unit/session-locator.spec.js +250 -0
- package/lib/test/unit/session-node-calls.spec.js +174 -0
- package/lib/test/unit/session-owner-resolver.spec.js +107 -0
- package/lib/test/unit/session-recovery-routing.spec.js +118 -0
- package/lib/test/unit/session-upgrade-guard.spec.js +571 -0
- package/lib/test/unit/shutdown-drain-remote.spec.js +58 -0
- package/lib/test/unit/socketServer.bearer.spec.js +3 -1
- package/lib/test/unit/stream-leave-route.spec.js +19 -7
- package/lib/test/unit/stream-own-device-row.spec.js +164 -0
- package/lib/test/unit/stream-stop-ownership.spec.js +17 -5
- package/lib/test/unit/stream-stop-recording.spec.js +17 -5
- package/lib/test/unit/stream-ticket-identity.spec.js +26 -6
- package/lib/test/unit/stream-watchdog-timers.spec.js +46 -0
- package/lib/test/unit/upgrade-router.spec.js +446 -0
- package/lib/test/unit/users-router.test.js +11 -8
- package/lib/test/unit/verify-credential.spec.js +4 -1
- package/lib/test/unit/webdriver-info-route.spec.js +64 -0
- package/lib/test/unit/ws-upgrade-routing-appium-server.spec.js +500 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -4
- package/prisma/migrations/20260929120000_device_claims/migration.sql +8 -0
- package/prisma/schema.prisma +9 -0
- package/lib/public/assets/BugReportButton-DKYsOdGV.js +0 -10
- package/lib/public/assets/device-explorer-DV8rWRxL.css +0 -1
- package/lib/src/proxy/wd-command-proxy.js +0 -221
|
@@ -51,7 +51,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
51
51
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
52
52
|
};
|
|
53
53
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
|
-
exports.SessionLifecycleService = void 0;
|
|
54
|
+
exports.SessionLifecycleService = exports.REMOTE_CREATE_TIMEOUT_MS = void 0;
|
|
55
55
|
const typedi_1 = require("typedi");
|
|
56
56
|
const uuid_1 = require("uuid");
|
|
57
57
|
const lodash_1 = __importDefault(require("lodash"));
|
|
@@ -73,24 +73,40 @@ const JwtKeyService_1 = require("./token/JwtKeyService");
|
|
|
73
73
|
const sessionIdentity_1 = require("./session/sessionIdentity");
|
|
74
74
|
const leaseSessionCaps_1 = require("./lease/leaseSessionCaps");
|
|
75
75
|
const sessionCredentials_1 = require("./session/sessionCredentials");
|
|
76
|
+
const nodeCreateCaps_1 = require("./session/nodeCreateCaps");
|
|
77
|
+
const xenonOptions_1 = require("./session/xenonOptions");
|
|
76
78
|
const leaseOverride_1 = require("./device-access/leaseOverride");
|
|
77
79
|
const callerTeamIds_1 = require("./device-access/callerTeamIds");
|
|
78
80
|
const queueVisibility_1 = require("./device-access/queueVisibility");
|
|
79
81
|
const appVisibility_1 = require("./device-access/appVisibility");
|
|
82
|
+
const LiveSessionOwners_1 = require("./device-access/LiveSessionOwners");
|
|
80
83
|
const appCapability_1 = require("./session/appCapability");
|
|
81
84
|
const CircuitBreaker_1 = require("../data-service/CircuitBreaker");
|
|
82
|
-
const
|
|
85
|
+
const nodeWebDriverUrl_1 = require("../gateway/nodeWebDriverUrl");
|
|
86
|
+
const hubSessionToken_1 = require("../gateway/hubSessionToken");
|
|
83
87
|
const device_store_1 = require("../data-service/device-store");
|
|
88
|
+
const deviceClaims_1 = require("../data-service/deviceClaims");
|
|
84
89
|
const LocalSession_1 = require("../sessions/LocalSession");
|
|
85
90
|
const CloudSession_1 = require("../sessions/CloudSession");
|
|
86
91
|
const RemoteSession_1 = require("../sessions/RemoteSession");
|
|
87
92
|
const SessionManager_1 = require("../sessions/SessionManager");
|
|
88
93
|
const event_manager_1 = require("../dashboard/event-manager");
|
|
94
|
+
const prisma_1 = require("../prisma");
|
|
89
95
|
const session_service_1 = require("../dashboard/services/session-service");
|
|
90
96
|
const SessionStatus_1 = require("../types/SessionStatus");
|
|
91
97
|
const SessionType_1 = __importDefault(require("../enums/SessionType"));
|
|
92
98
|
const async_lock_1 = __importDefault(require("async-lock"));
|
|
93
99
|
const base_driver_1 = require("@appium/base-driver");
|
|
100
|
+
/**
|
|
101
|
+
* How long the hub waits for a node (or cloud provider) to create a session.
|
|
102
|
+
* A create is sent once (InternalRequestConfig `retry: false`): the node may
|
|
103
|
+
* still be creating when an ordinary request would give up, and a second
|
|
104
|
+
* create starts a second session holding the phone. A first UiAutomator2 or
|
|
105
|
+
* WebDriverAgent install takes minutes. It ends before the phone's pending
|
|
106
|
+
* claim can expire (PENDING_CLAIM_TIMEOUT_MS), so the idle sweeper never frees
|
|
107
|
+
* a phone whose create the hub is still waiting for.
|
|
108
|
+
*/
|
|
109
|
+
exports.REMOTE_CREATE_TIMEOUT_MS = deviceClaims_1.PENDING_CLAIM_TIMEOUT_MS - 2 * 60000;
|
|
94
110
|
const commandsQueueGuard = new async_lock_1.default();
|
|
95
111
|
// Serializes concurrent deleteSession cleanup for the same sessionId so
|
|
96
112
|
// that onSessionStopped events don't fire twice if two clients race.
|
|
@@ -99,9 +115,52 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
99
115
|
constructor() {
|
|
100
116
|
this.logger = logger_1.default.scope('SessionLifecycleService');
|
|
101
117
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Create a session in one call: prepare it (allocate a phone), then
|
|
120
|
+
* complete it here through `next` (a phone this server drives) or on the
|
|
121
|
+
* phone's node (one another server drives).
|
|
122
|
+
*
|
|
123
|
+
* The session gateway runs the same steps with Appium's route between them
|
|
124
|
+
* (gateway/sessionCreate.ts): it prepares, answers a remote phone itself,
|
|
125
|
+
* and hands a local phone's allocation to XenonPlugin.createSession, which
|
|
126
|
+
* completes it. The plugin calls this only for a create that did not come
|
|
127
|
+
* through the gateway, with `localOnly`: a phone on another server is then
|
|
128
|
+
* given back and refused, since answering for Appium without its driver is
|
|
129
|
+
* what made Appium's umbrella fail (a 500 while the node kept the session).
|
|
130
|
+
*/
|
|
131
|
+
createSession(next_1, driver_1, caps_1) {
|
|
132
|
+
return __awaiter(this, arguments, void 0, function* (next, driver, caps, opts = {}) {
|
|
133
|
+
const allocation = yield this.prepareSession(caps);
|
|
134
|
+
if (!allocation.remote)
|
|
135
|
+
return this.completeLocalSession(allocation, next, driver, caps);
|
|
136
|
+
if (opts.localOnly) {
|
|
137
|
+
yield this.releaseAllocation(allocation);
|
|
138
|
+
throw new base_driver_1.errors.SessionNotCreatedError(`Device ${allocation.device.udid} is driven by another server ` +
|
|
139
|
+
`(${allocation.device.host}). Xenon creates sessions there in its session gateway, ` +
|
|
140
|
+
'which did not handle this request; the device was released.');
|
|
141
|
+
}
|
|
142
|
+
return this.completeRemoteSession(allocation);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The first half of a create: everything up to and including allocating a
|
|
147
|
+
* phone. It takes the credentials out of `caps` (in place) before anything
|
|
148
|
+
* reads them, checks them (or the hub's grant), validates the capabilities,
|
|
149
|
+
* resolves an uploaded app, writes the pending-session row and allocates.
|
|
150
|
+
* Throws to refuse, leaving no pending row and no busy phone.
|
|
151
|
+
*
|
|
152
|
+
* `hubGrant` is the verified grant of a create the hub forwarded (a node
|
|
153
|
+
* only; gateway/sessionCreate.ts). Each instance has its own database, so a
|
|
154
|
+
* node can't check the client's key: the hub checked it, applied the team
|
|
155
|
+
* rule and the session-token gate, and resolved any lease. The grant is
|
|
156
|
+
* then the session's credential: it names the owner, and the one phone on
|
|
157
|
+
* this node the create may have.
|
|
158
|
+
*
|
|
159
|
+
* What it returns must be completed (completeLocalSession /
|
|
160
|
+
* completeRemoteSession) or released (releaseAllocation).
|
|
161
|
+
*/
|
|
162
|
+
prepareSession(caps_1) {
|
|
163
|
+
return __awaiter(this, arguments, void 0, function* (caps, opts = {}) {
|
|
105
164
|
// Fail fast during graceful shutdown so clients get a clear error instead
|
|
106
165
|
// of their request hanging until the process dies. Lazy import to avoid
|
|
107
166
|
// a cycle (ShutdownCoordinator -> SessionLifecycleService -> this file).
|
|
@@ -116,7 +175,13 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
116
175
|
// driver, the Session row, the dashboard, the logs — and hand them only to
|
|
117
176
|
// the checks that need them.
|
|
118
177
|
const credentials = (0, sessionCredentials_1.takeSessionCredentials)(caps);
|
|
119
|
-
const
|
|
178
|
+
const { hubGrant } = opts;
|
|
179
|
+
yield this.assertCreateCameFromHub(hubGrant);
|
|
180
|
+
if (hubGrant)
|
|
181
|
+
this.assertGrantNamesCapsPhone(caps, hubGrant);
|
|
182
|
+
const authResult = hubGrant
|
|
183
|
+
? this.authorizeHubGrant(hubGrant)
|
|
184
|
+
: yield this.authorizeSessionRequest(caps, credentials);
|
|
120
185
|
const context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
121
186
|
const pluginArgs = context.pluginArgs;
|
|
122
187
|
this.logger.debug(`📱 pluginArgs: ${JSON.stringify(pluginArgs)}`);
|
|
@@ -174,76 +239,207 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
174
239
|
}
|
|
175
240
|
}));
|
|
176
241
|
yield (0, device_service_1.updateDeviceProgress)(device.udid, device.host, 'Allocating node resources...');
|
|
177
|
-
let session;
|
|
178
|
-
const isRemoteOrCloudSession = !device.nodeId || device.nodeId !== context.nodeId;
|
|
179
242
|
this.logger.debug(`device.host: ${device.host} and pluginArgs.bindHostOrIp: ${pluginArgs.bindHostOrIp}`);
|
|
243
|
+
const allocation = {
|
|
244
|
+
caps,
|
|
245
|
+
device,
|
|
246
|
+
remote: !device.nodeId || device.nodeId !== context.nodeId,
|
|
247
|
+
pendingSessionId,
|
|
248
|
+
apiKeyId: authResult.apiKeyId,
|
|
249
|
+
userId: authResult.userId,
|
|
250
|
+
appDownload,
|
|
251
|
+
};
|
|
252
|
+
// The grant opens one phone, on this node. A token taken to another node
|
|
253
|
+
// (an emulator's udid repeats across machines), or naming a phone this
|
|
254
|
+
// node does not drive, is refused, and the phone given back.
|
|
255
|
+
if (hubGrant &&
|
|
256
|
+
(allocation.remote || device.udid !== hubGrant.udid || device.host !== hubGrant.host)) {
|
|
257
|
+
yield this.releaseAllocation(allocation);
|
|
258
|
+
this.logger.error(`❌ Rejecting session: the hub's token is for ${hubGrant.udid} at ${hubGrant.host}, ` +
|
|
259
|
+
`and this node allocated ${device.udid} at ${device.host}`);
|
|
260
|
+
throw new base_driver_1.errors.InvalidArgumentError("session rejected: the hub's token for this session names another device");
|
|
261
|
+
}
|
|
262
|
+
return allocation;
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Complete a create on the phone's node (or cloud provider), and record the
|
|
267
|
+
* session here. On failure the phone is released and the error thrown.
|
|
268
|
+
*/
|
|
269
|
+
completeRemoteSession(allocation) {
|
|
270
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
271
|
+
const { device, caps, appDownload } = allocation;
|
|
272
|
+
let session;
|
|
180
273
|
try {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
:
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (appDownload)
|
|
203
|
-
(0, appCapability_1.setAppCapability)(caps, yield this.driverAppUrl(appDownload));
|
|
204
|
-
try {
|
|
205
|
-
session = yield next();
|
|
206
|
-
}
|
|
207
|
-
finally {
|
|
208
|
-
if (appDownload)
|
|
209
|
-
(0, appCapability_1.setAppCapability)(caps, appDownload.url);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
274
|
+
this.logger.debug(`📱 Forwarding session request to ${device.host}`);
|
|
275
|
+
yield (0, device_service_1.updateDeviceProgress)(device.udid, device.host, 'Forwarding to remote node...');
|
|
276
|
+
// A peer Xenon node gets a create that stands on its own (capsForNode:
|
|
277
|
+
// no credentials, no lease id, the phone pinned), with the hub's token
|
|
278
|
+
// naming the owner this hub verified, since it can't check the client's
|
|
279
|
+
// key in its own database. A cloud provider is not a Xenon node: it
|
|
280
|
+
// gets the caps without the credentials, and no token.
|
|
281
|
+
const isPeerNode = !device.cloud && !!device.nodeId;
|
|
282
|
+
const forwardCaps = isPeerNode ? (0, nodeCreateCaps_1.capsForNode)(caps, device.udid) : caps;
|
|
283
|
+
const hubToken = isPeerNode
|
|
284
|
+
? yield typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer).createTokenFor({
|
|
285
|
+
userId: allocation.userId,
|
|
286
|
+
udid: device.udid,
|
|
287
|
+
host: device.host,
|
|
288
|
+
})
|
|
289
|
+
: null;
|
|
290
|
+
// The node's driver downloads the app from this hub, so the copy it
|
|
291
|
+
// is sent carries the ticket; `caps` keeps the plain URL.
|
|
292
|
+
session = yield this.forwardSessionRequest(device, appDownload
|
|
293
|
+
? (0, appCapability_1.withAppCapability)(forwardCaps, yield this.driverAppUrl(appDownload))
|
|
294
|
+
: forwardCaps, hubToken);
|
|
212
295
|
}
|
|
213
296
|
catch (err) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
297
|
+
yield this.failedCreate(allocation, err);
|
|
298
|
+
throw err;
|
|
299
|
+
}
|
|
300
|
+
return this.finishCreate(allocation, session, undefined);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Complete a create on a phone this server drives: `next` is Appium's own
|
|
305
|
+
* createSession, and `caps` the capabilities its driver reads. On failure
|
|
306
|
+
* the phone is released and the error thrown.
|
|
307
|
+
*/
|
|
308
|
+
completeLocalSession(allocation, next, driver, caps) {
|
|
309
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
310
|
+
const { device, appDownload } = allocation;
|
|
311
|
+
let session;
|
|
312
|
+
try {
|
|
313
|
+
this.logger.debug('📱 Creating session on the same node');
|
|
314
|
+
yield this.handleLocalWDAProvisioning(device, caps);
|
|
315
|
+
yield (0, device_service_1.updateDeviceProgress)(device.udid, device.host, 'Finalizing session bootstrap...');
|
|
316
|
+
// The driver reads these same caps and downloads the app while it
|
|
317
|
+
// starts, so the ticket is in them for this call only.
|
|
318
|
+
if (appDownload)
|
|
319
|
+
(0, appCapability_1.setAppCapability)(caps, yield this.driverAppUrl(appDownload));
|
|
222
320
|
try {
|
|
223
|
-
yield (
|
|
224
|
-
yield (0, device_service_1.unblockDevice)(device.udid, device.host);
|
|
225
|
-
yield (0, device_service_1.updateDeviceProgress)(device.udid, device.host, '');
|
|
226
|
-
if (isRemoteOrCloudSession) {
|
|
227
|
-
typedi_1.Container.get(CircuitBreaker_1.CircuitBreaker).recordFailure(device.host);
|
|
228
|
-
}
|
|
321
|
+
session = yield next();
|
|
229
322
|
}
|
|
230
|
-
|
|
231
|
-
|
|
323
|
+
finally {
|
|
324
|
+
if (appDownload)
|
|
325
|
+
(0, appCapability_1.setAppCapability)(caps, appDownload.url);
|
|
232
326
|
}
|
|
327
|
+
}
|
|
328
|
+
catch (err) {
|
|
329
|
+
yield this.failedCreate(allocation, err);
|
|
233
330
|
throw err;
|
|
234
331
|
}
|
|
332
|
+
return this.finishCreate(Object.assign(Object.assign({}, allocation), { caps }), session, driver);
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Give back a phone allocated for a session that was never created: its
|
|
337
|
+
* pending-session row goes and the phone is free again. Only the claim this
|
|
338
|
+
* allocation took is released: if it timed out and the phone went on to
|
|
339
|
+
* another session, that session keeps it. Never throws.
|
|
340
|
+
*/
|
|
341
|
+
releaseAllocation(allocation) {
|
|
342
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
343
|
+
var _a;
|
|
344
|
+
const { device } = allocation;
|
|
345
|
+
try {
|
|
346
|
+
yield (0, pending_sessions_service_1.removePendingSession)(allocation.pendingSessionId);
|
|
347
|
+
yield (0, device_service_1.releasePendingClaim)(device);
|
|
348
|
+
yield (0, device_service_1.updateDeviceProgress)(device.udid, device.host, '');
|
|
349
|
+
}
|
|
350
|
+
catch (cleanupErr) {
|
|
351
|
+
this.logger.warn(`Cleanup after failed session creation had issues: ${(_a = cleanupErr === null || cleanupErr === void 0 ? void 0 : cleanupErr.message) !== null && _a !== void 0 ? _a : cleanupErr}`);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
// A THROWN error from the driver's createSession (next()) or the remote
|
|
356
|
+
// forward leaves the device locked: allocation already set busy=true, and
|
|
357
|
+
// on the throw path neither finalizeSession nor handleSessionFailure runs,
|
|
358
|
+
// so without this the device gets stuck busy with no session. (finishCreate
|
|
359
|
+
// covers the separate case where next() RETURNS a W3C error object rather
|
|
360
|
+
// than throwing.)
|
|
361
|
+
failedCreate(allocation, err) {
|
|
362
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
363
|
+
var _a;
|
|
364
|
+
const { device } = allocation;
|
|
365
|
+
this.logger.error(`❌ Session creation failed for device ${device.udid}: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}. Unblocking device.`);
|
|
366
|
+
yield this.releaseAllocation(allocation);
|
|
367
|
+
if (allocation.remote) {
|
|
368
|
+
typedi_1.Container.get(CircuitBreaker_1.CircuitBreaker).recordFailure(device.host);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
finishCreate(allocation, session, driver) {
|
|
373
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
374
|
+
const { device, caps, remote } = allocation;
|
|
235
375
|
this.logger.debug('📱 Session response: ', JSON.stringify(session));
|
|
236
|
-
this.logger.debug(`📱 Removing pending session with capability_id: ${pendingSessionId}`);
|
|
237
|
-
yield (0, pending_sessions_service_1.removePendingSession)(pendingSessionId);
|
|
376
|
+
this.logger.debug(`📱 Removing pending session with capability_id: ${allocation.pendingSessionId}`);
|
|
377
|
+
yield (0, pending_sessions_service_1.removePendingSession)(allocation.pendingSessionId);
|
|
238
378
|
if (this.isCreateSessionResponseInternal(session)) {
|
|
239
|
-
yield this.finalizeSession(session, device, caps, driver,
|
|
379
|
+
yield this.finalizeSession(session, device, caps, driver, remote, allocation.apiKeyId, allocation.userId);
|
|
240
380
|
}
|
|
241
381
|
else {
|
|
242
|
-
yield this.handleSessionFailure(session, device,
|
|
382
|
+
yield this.handleSessionFailure(session, device, remote);
|
|
243
383
|
}
|
|
244
384
|
return session;
|
|
245
385
|
});
|
|
246
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* Sessions on a node's phones go through its hub, which owns their team
|
|
389
|
+
* rules, reservations and blocks and is where auth is enforced. So a node
|
|
390
|
+
* (a server with `hub`) refuses a create that doesn't carry the hub's
|
|
391
|
+
* verified grant: one sent to the node directly, or one that reached the
|
|
392
|
+
* plugin without the session gateway, which is where the grant is checked.
|
|
393
|
+
*
|
|
394
|
+
* Only with auth enabled. With auth disabled a node checks no credential,
|
|
395
|
+
* the hub's token included (sessionCreate.ts), so a create from the hub and
|
|
396
|
+
* one sent directly look alike, and refusing would protect nothing while
|
|
397
|
+
* breaking what such a node is used for: running a driver on it directly
|
|
398
|
+
* during local development. It works there as it always has.
|
|
399
|
+
*/
|
|
400
|
+
assertCreateCameFromHub(hubGrant) {
|
|
401
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
402
|
+
const { hub } = typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs;
|
|
403
|
+
if (hub === undefined || hubGrant)
|
|
404
|
+
return;
|
|
405
|
+
const { config: xenonConfig } = yield Promise.resolve().then(() => __importStar(require('../config')));
|
|
406
|
+
if (xenonConfig.authDisabled === true)
|
|
407
|
+
return;
|
|
408
|
+
this.logger.warn(`Refusing a session created directly on this node: it carries no grant from the hub (${hub}).`);
|
|
409
|
+
throw new base_driver_1.errors.SessionNotCreatedError(`This server is a node of the Xenon hub at ${hub}. Create sessions through the hub: ` +
|
|
410
|
+
'it applies the team rules, reservations and blocks for this node’s devices, and a ' +
|
|
411
|
+
'node refuses a session that does not come from it.');
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* A grant is for the phone the hub pinned in the caps it forwarded
|
|
416
|
+
* (capsForNode). Caps that name another one, or a list to pick from, are
|
|
417
|
+
* not a create the hub sent: refused before anything is allocated.
|
|
418
|
+
*/
|
|
419
|
+
assertGrantNamesCapsPhone(caps, grant) {
|
|
420
|
+
var _a, _b;
|
|
421
|
+
const merged = (0, xenonOptions_1.mergedFirstMatch)(caps);
|
|
422
|
+
if (merged['appium:udid'] === grant.udid && merged['appium:udids'] === undefined)
|
|
423
|
+
return;
|
|
424
|
+
this.logger.error(`❌ Rejecting session: the hub's token is for ${grant.udid}, and the capabilities name ` +
|
|
425
|
+
`${(_b = (_a = merged['appium:udids']) !== null && _a !== void 0 ? _a : merged['appium:udid']) !== null && _b !== void 0 ? _b : 'no device'}`);
|
|
426
|
+
throw new base_driver_1.errors.InvalidArgumentError("session rejected: the hub's token for this session names another device");
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* The session's identity when the hub forwarded the create with its grant:
|
|
430
|
+
* the owner the hub verified, unscoped (the hub applied the team rule), and
|
|
431
|
+
* no lease override (the hub resolved the lease).
|
|
432
|
+
*/
|
|
433
|
+
authorizeHubGrant(grant) {
|
|
434
|
+
return {
|
|
435
|
+
apiKeyId: null,
|
|
436
|
+
userId: grant.userId,
|
|
437
|
+
callerTeamIds: undefined,
|
|
438
|
+
scoped: false,
|
|
439
|
+
leaseAccess: () => __awaiter(this, void 0, void 0, function* () { return ({ canOverride: false, teamIds: undefined }); }),
|
|
440
|
+
requester: { userId: grant.userId, teamId: null },
|
|
441
|
+
};
|
|
442
|
+
}
|
|
247
443
|
/**
|
|
248
444
|
* Resolves an uploaded-app id the session names to its plain download URL,
|
|
249
445
|
* written into `caps` in place, when the session may see the app (by
|
|
@@ -406,8 +602,8 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
406
602
|
const requestedTeam = (0, XenonCapabilityManager_1.extractTeamCap)(caps);
|
|
407
603
|
if (requestedTeam) {
|
|
408
604
|
if (callerTeamIds !== undefined && !callerTeamIds.includes(requestedTeam)) {
|
|
409
|
-
this.logger.error(`Rejecting session:
|
|
410
|
-
throw new base_driver_1.errors.InvalidArgumentError(`
|
|
605
|
+
this.logger.error(`Rejecting session: xe:options.team=${requestedTeam} is not one of the caller's teams (${callerTeamIds.join(', ') || 'shared pool only'})`);
|
|
606
|
+
throw new base_driver_1.errors.InvalidArgumentError(`xe:options.team '${requestedTeam}' is not allowed for this API key`);
|
|
411
607
|
}
|
|
412
608
|
return {
|
|
413
609
|
apiKeyId: row.id,
|
|
@@ -597,21 +793,29 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
597
793
|
'xenon.udid': device.udid,
|
|
598
794
|
});
|
|
599
795
|
}
|
|
600
|
-
|
|
601
|
-
|
|
796
|
+
// The session takes over the pending claim its phone was allocated with.
|
|
797
|
+
yield (0, device_service_1.claimDeviceForSession)(device, sessionId, {
|
|
602
798
|
session_id: sessionId,
|
|
603
799
|
lastCmdExecutedAt: new Date().getTime(),
|
|
604
800
|
sessionStartTime: new Date().getTime(),
|
|
605
801
|
sessionProgress: 'Session Active',
|
|
606
802
|
});
|
|
803
|
+
// Where the session gateway sends this session's commands: the node's own
|
|
804
|
+
// base path, which need not be this hub's.
|
|
805
|
+
let webDriverUrl;
|
|
607
806
|
if (isRemote) {
|
|
608
807
|
typedi_1.Container.get(CircuitBreaker_1.CircuitBreaker).recordSuccess(device.host);
|
|
609
|
-
(0,
|
|
808
|
+
webDriverUrl = yield (0, nodeWebDriverUrl_1.nodeWebDriverUrl)(device, context.nodeBasePath);
|
|
610
809
|
}
|
|
611
810
|
const freshDevice = yield this.getFreshDevice(device);
|
|
612
|
-
const sessionInstance = this.createSessionInstance(sessionId, freshDevice, sessionResponse, xenonCapabilities, driver);
|
|
811
|
+
const sessionInstance = this.createSessionInstance(sessionId, freshDevice, sessionResponse, xenonCapabilities, driver, webDriverUrl);
|
|
613
812
|
sessionInstance.apiKeyId = apiKeyId;
|
|
614
813
|
sessionInstance.userId = userId;
|
|
814
|
+
// A session this server drives: its owner is known while it runs, row or
|
|
815
|
+
// not (a node writes none for the hub's sessions; LiveSessionOwners).
|
|
816
|
+
if (sessionInstance instanceof LocalSession_1.LocalSession) {
|
|
817
|
+
typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).record(sessionId, userId);
|
|
818
|
+
}
|
|
615
819
|
yield this.applyPostSessionLogic(sessionInstance, xenonCapabilities, freshDevice);
|
|
616
820
|
});
|
|
617
821
|
}
|
|
@@ -628,7 +832,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
628
832
|
}
|
|
629
833
|
});
|
|
630
834
|
}
|
|
631
|
-
createSessionInstance(sessionId, device, response, caps, driver) {
|
|
835
|
+
createSessionInstance(sessionId, device, response, caps, driver, webDriverUrl) {
|
|
632
836
|
const context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
633
837
|
const sessionOptions = {
|
|
634
838
|
sessionId,
|
|
@@ -636,7 +840,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
636
840
|
sessionResponse: response,
|
|
637
841
|
xenonOption: caps,
|
|
638
842
|
};
|
|
639
|
-
const nodeWebdriverUrl = (0, helpers_1.nodeUrl)(device, context.nodeBasePath);
|
|
843
|
+
const nodeWebdriverUrl = webDriverUrl !== null && webDriverUrl !== void 0 ? webDriverUrl : (0, helpers_1.nodeUrl)(device, context.nodeBasePath);
|
|
640
844
|
if (device.nodeId === context.nodeId) {
|
|
641
845
|
return new LocalSession_1.LocalSession(Object.assign(Object.assign({}, sessionOptions), { driver }));
|
|
642
846
|
}
|
|
@@ -686,17 +890,53 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
686
890
|
this.logger.warn(`⚠️ Failed to start video recording for ${session.getId()}: ${err.message}`);
|
|
687
891
|
}
|
|
688
892
|
}
|
|
689
|
-
|
|
893
|
+
// A session another server runs is always registered, whatever the
|
|
894
|
+
// dashboard and video settings: SESSION_MANAGER is where the session
|
|
895
|
+
// gateway finds it to forward its commands.
|
|
896
|
+
const routedElsewhere = session.getType() !== SessionType_1.default.LOCAL;
|
|
897
|
+
if (routedElsewhere ||
|
|
898
|
+
(isDashboardEnabled && shouldSaveLogs) ||
|
|
899
|
+
(isVideoRecordingEnabled && shouldSaveLogs)) {
|
|
690
900
|
SessionManager_1.SESSION_MANAGER.addSession(session.getId(), session);
|
|
691
|
-
if (this.isHub(context.pluginArgs) && isDashboardEnabled) {
|
|
901
|
+
if (this.isHub(context.pluginArgs) && isDashboardEnabled && shouldSaveLogs) {
|
|
692
902
|
yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStarted(caps, session, device);
|
|
693
903
|
}
|
|
904
|
+
else if (routedElsewhere) {
|
|
905
|
+
yield this.recordRoutedSession(caps, session, device);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* The row a hub keeps for a session it routes to a node or a cloud when the
|
|
912
|
+
* dashboard's own (onSessionStarted) isn't written: with the dashboard off,
|
|
913
|
+
* and for every cloud session. After a restart the hub finds where the
|
|
914
|
+
* session runs, and who owns it, only from this row (SessionLocator,
|
|
915
|
+
* recoverActiveSessions, SessionOwnerResolver): without it every node
|
|
916
|
+
* session was sent to the hub's own Appium, which had no such session.
|
|
917
|
+
*
|
|
918
|
+
* Only what routing and ownership need: no profiling, logs or dashboard
|
|
919
|
+
* event. onSessionStopped closes it like any other row. A local session's
|
|
920
|
+
* row is still the dashboard's alone. A failed write is logged, not
|
|
921
|
+
* thrown: the session exists, and is routed from memory until a restart.
|
|
922
|
+
*/
|
|
923
|
+
recordRoutedSession(caps, session, device) {
|
|
924
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
925
|
+
var _a, _b, _c;
|
|
926
|
+
try {
|
|
927
|
+
yield prisma_1.prisma.session.create({
|
|
928
|
+
data: Object.assign(Object.assign({ id: session.getId(), name: caps[XenonCapabilityManager_1.XENON_CAPABILITIES.SESSION_NAME] || undefined }, (0, event_manager_1.storedSessionCapabilities)(lodash_1.default.assign({}, session.getCapabilities()))), { node_id: device.nodeId || '', has_live_video: false, video_recording_enabled: caps[XenonCapabilityManager_1.XENON_CAPABILITIES.VIDEO_RECORDING] === true, device_udid: device.udid || '', device_platform: device.platform || '', device_version: device.sdk || '', device_name: device.name, status: 'running', api_key_id: (_a = session.apiKeyId) !== null && _a !== void 0 ? _a : null, user_id: (_b = session.userId) !== null && _b !== void 0 ? _b : null }),
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
catch (err) {
|
|
932
|
+
this.logger.warn(`Could not record session ${session.getId()} on ${device.udid}: ${(_c = err === null || err === void 0 ? void 0 : err.message) !== null && _c !== void 0 ? _c : err}. ` +
|
|
933
|
+
'It is routed from memory, and a restart of this hub loses it.');
|
|
694
934
|
}
|
|
695
935
|
});
|
|
696
936
|
}
|
|
697
937
|
handleSessionFailure(session, device, isRemote) {
|
|
698
938
|
return __awaiter(this, void 0, void 0, function* () {
|
|
699
|
-
yield (0, device_service_1.
|
|
939
|
+
yield (0, device_service_1.releasePendingClaim)(device);
|
|
700
940
|
yield (0, device_service_1.updateDeviceProgress)(device.udid, device.host, '');
|
|
701
941
|
if (isRemote) {
|
|
702
942
|
typedi_1.Container.get(CircuitBreaker_1.CircuitBreaker).recordFailure(device.host);
|
|
@@ -704,15 +944,26 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
704
944
|
this.throwProperError(session, device.host);
|
|
705
945
|
});
|
|
706
946
|
}
|
|
707
|
-
|
|
708
|
-
|
|
947
|
+
/**
|
|
948
|
+
* POST the create to the phone's node (or cloud provider). `hubToken` is
|
|
949
|
+
* this hub's create token for a peer Xenon node (HubSessionTokenIssuer),
|
|
950
|
+
* sent as `x-xenon-hub-token`; nothing else of the client's goes with it.
|
|
951
|
+
*/
|
|
952
|
+
forwardSessionRequest(device_1, caps_1) {
|
|
953
|
+
return __awaiter(this, arguments, void 0, function* (device, caps, hubToken = null) {
|
|
709
954
|
const context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
710
|
-
const remoteUrl = `${(0,
|
|
955
|
+
const remoteUrl = `${yield (0, nodeWebDriverUrl_1.nodeWebDriverUrl)(device, context.nodeBasePath)}/session`;
|
|
956
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
957
|
+
if (hubToken)
|
|
958
|
+
headers[hubSessionToken_1.HUB_TOKEN_HEADER] = hubToken;
|
|
959
|
+
// Sent once, with a create's timeout (REMOTE_CREATE_TIMEOUT_MS).
|
|
711
960
|
const config = {
|
|
712
961
|
method: 'post',
|
|
713
962
|
url: remoteUrl,
|
|
714
|
-
headers
|
|
963
|
+
headers,
|
|
715
964
|
data: { capabilities: caps },
|
|
965
|
+
timeout: exports.REMOTE_CREATE_TIMEOUT_MS,
|
|
966
|
+
retry: false,
|
|
716
967
|
};
|
|
717
968
|
if (context.pluginArgs.proxy) {
|
|
718
969
|
this.logger.info(`Added proxy to axios config: ${JSON.stringify(context.pluginArgs.proxy)}`);
|
|
@@ -793,6 +1044,11 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
793
1044
|
typeof session === 'object' &&
|
|
794
1045
|
Object.prototype.hasOwnProperty.call(session, 'error')) {
|
|
795
1046
|
let errorMessage = session.error;
|
|
1047
|
+
// Appium answers a refused create as `{ error: <Error> }`. Rethrow it, so
|
|
1048
|
+
// the client gets Appium's reason and status; JSON.stringify of an Error
|
|
1049
|
+
// is `{}`.
|
|
1050
|
+
if (errorMessage instanceof Error)
|
|
1051
|
+
throw errorMessage;
|
|
796
1052
|
if (typeof errorMessage === 'object') {
|
|
797
1053
|
errorMessage = JSON.stringify(errorMessage);
|
|
798
1054
|
}
|
|
@@ -818,7 +1074,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
818
1074
|
// which releases ports and archives video twice.
|
|
819
1075
|
if (sessionId) {
|
|
820
1076
|
yield sessionCleanupLock.acquire(sessionId, () => __awaiter(this, void 0, void 0, function* () {
|
|
821
|
-
yield (0, device_service_1.
|
|
1077
|
+
yield (0, device_service_1.releaseSessionDevices)(sessionId);
|
|
822
1078
|
this.logger.info(`📱 Unblocking the device that is blocked for session ${sessionId}`);
|
|
823
1079
|
const session = SessionManager_1.SESSION_MANAGER.getSession(sessionId);
|
|
824
1080
|
if (session && !session.isStopping) {
|
|
@@ -847,6 +1103,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
847
1103
|
}
|
|
848
1104
|
finally {
|
|
849
1105
|
if (sessionId) {
|
|
1106
|
+
typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).forget(sessionId);
|
|
850
1107
|
yield sessionCleanupLock.acquire(sessionId, () => __awaiter(this, void 0, void 0, function* () {
|
|
851
1108
|
const session = SessionManager_1.SESSION_MANAGER.getSession(sessionId);
|
|
852
1109
|
if (!session) {
|
|
@@ -872,6 +1129,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
872
1129
|
}
|
|
873
1130
|
yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStopped(sessionId, status, reason);
|
|
874
1131
|
SessionManager_1.SESSION_MANAGER.removeSession(sessionId);
|
|
1132
|
+
typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer).forget(sessionId);
|
|
875
1133
|
try {
|
|
876
1134
|
const { getSessionById } = yield Promise.resolve().then(() => __importStar(require('../dashboard/services/session-service')));
|
|
877
1135
|
const sessionData = yield getSessionById(sessionId);
|
|
@@ -956,7 +1214,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
956
1214
|
return __awaiter(this, void 0, void 0, function* () {
|
|
957
1215
|
yield sessionCleanupLock.acquire(sessionId, () => __awaiter(this, void 0, void 0, function* () {
|
|
958
1216
|
try {
|
|
959
|
-
yield (0, device_service_1.
|
|
1217
|
+
yield (0, device_service_1.releaseSessionDevices)(sessionId);
|
|
960
1218
|
}
|
|
961
1219
|
catch (err) {
|
|
962
1220
|
this.logger.warn(`[shutdown] unblock failed for ${sessionId}: ${err.message}`);
|
|
@@ -979,6 +1237,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
979
1237
|
this.logger.warn(`[shutdown] onSessionStopped failed for ${sessionId}: ${err.message}`);
|
|
980
1238
|
}
|
|
981
1239
|
SessionManager_1.SESSION_MANAGER.removeSession(sessionId);
|
|
1240
|
+
typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).forget(sessionId);
|
|
982
1241
|
}));
|
|
983
1242
|
});
|
|
984
1243
|
}
|
|
@@ -23,6 +23,7 @@ const typedi_1 = require("typedi");
|
|
|
23
23
|
const logger_1 = __importDefault(require("../logger"));
|
|
24
24
|
const SessionManager_1 = require("../sessions/SessionManager");
|
|
25
25
|
const SessionLifecycleService_1 = require("./SessionLifecycleService");
|
|
26
|
+
const SessionType_1 = __importDefault(require("../enums/SessionType"));
|
|
26
27
|
// Graceful shutdown phase that runs BEFORE the existing infrastructure
|
|
27
28
|
// teardown in index.ts:cleanup(). Active sessions get a bounded chance to
|
|
28
29
|
// archive their video + release ports + mark themselves failed, so we don't
|
|
@@ -48,7 +49,12 @@ let ShutdownCoordinator = class ShutdownCoordinator {
|
|
|
48
49
|
return { attempted: 0, completed: 0 };
|
|
49
50
|
}
|
|
50
51
|
this.draining = true;
|
|
51
|
-
|
|
52
|
+
// Only this server's own sessions: they die with the process. A node's or
|
|
53
|
+
// a cloud provider's session keeps running there, and the restarted hub
|
|
54
|
+
// routes it again from its database (SessionManager.recoverActiveSessions).
|
|
55
|
+
// Finalizing it here released the phone and closed the row, so recovery
|
|
56
|
+
// found nothing, and the node held a session no client could reach.
|
|
57
|
+
const sessions = SessionManager_1.SESSION_MANAGER.getAllSessions().filter((session) => session.getType() === SessionType_1.default.LOCAL);
|
|
52
58
|
if (sessions.length === 0) {
|
|
53
59
|
this.logger.info('No active sessions to drain');
|
|
54
60
|
return { attempted: 0, completed: 0 };
|
|
@@ -32,6 +32,9 @@ const EventLogService_1 = require("./EventLogService");
|
|
|
32
32
|
const SocketEvents_1 = require("../enums/SocketEvents");
|
|
33
33
|
const callerTeamIds_1 = require("./device-access/callerTeamIds");
|
|
34
34
|
const DeviceTeamResolver_1 = require("./device-access/DeviceTeamResolver");
|
|
35
|
+
const upgradeRouter_1 = require("../app/ws/upgradeRouter");
|
|
36
|
+
/** socket.io's default path, which the dashboard and nodes connect to. */
|
|
37
|
+
const SOCKET_IO_PATH = '/socket.io';
|
|
35
38
|
const SESSION_COOKIE = 'xenon_dashboard_session';
|
|
36
39
|
function readCookie(cookieHeader, name) {
|
|
37
40
|
if (!cookieHeader)
|
|
@@ -54,7 +57,13 @@ let SocketServer = SocketServer_1 = class SocketServer {
|
|
|
54
57
|
this.deliveries = new Map();
|
|
55
58
|
}
|
|
56
59
|
initialize(server) {
|
|
57
|
-
|
|
60
|
+
// socket.io (engine.io) takes its websocket transport by adding its own
|
|
61
|
+
// `upgrade` listener, which would also see every other upgrade and end
|
|
62
|
+
// the ones it doesn't own after 1 s. The upgrade router moves that
|
|
63
|
+
// listener behind socket.io's path, engine.io's own test (a prefix match
|
|
64
|
+
// on the raw URL), so it sees only its own upgrades (upgradeRouter.ts).
|
|
65
|
+
this.io = (0, upgradeRouter_1.upgradeRouterFor)(server).adopt('socket.io', (req) => { var _a; return String((_a = req.url) !== null && _a !== void 0 ? _a : '').startsWith(`${SOCKET_IO_PATH}/`); }, () => new socket_io_1.Server(server, {
|
|
66
|
+
path: SOCKET_IO_PATH,
|
|
58
67
|
// Same-origin only. Browsers on a different origin are blocked at the
|
|
59
68
|
// handshake; server-to-server node connections send no Origin header
|
|
60
69
|
// and are unaffected. Matches the apiRouter cors({origin:false}) policy.
|
|
@@ -62,7 +71,7 @@ let SocketServer = SocketServer_1 = class SocketServer {
|
|
|
62
71
|
origin: false,
|
|
63
72
|
methods: ['GET', 'POST'],
|
|
64
73
|
},
|
|
65
|
-
});
|
|
74
|
+
}));
|
|
66
75
|
this.io.use((socket, next) => __awaiter(this, void 0, void 0, function* () {
|
|
67
76
|
try {
|
|
68
77
|
const identity = yield this.authenticate(socket);
|