@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
package/lib/src/device-utils.js
CHANGED
|
@@ -66,6 +66,7 @@ exports.unblockCandidateDevices = unblockCandidateDevices;
|
|
|
66
66
|
exports.releaseBlockedDevices = releaseBlockedDevices;
|
|
67
67
|
exports.setupCronReleaseBlockedDevices = setupCronReleaseBlockedDevices;
|
|
68
68
|
exports.setupCronUpdateDeviceList = setupCronUpdateDeviceList;
|
|
69
|
+
exports.unregisterNodeFromHub = unregisterNodeFromHub;
|
|
69
70
|
exports.setupCronLocalDiscovery = setupCronLocalDiscovery;
|
|
70
71
|
exports.cleanPendingSessions = cleanPendingSessions;
|
|
71
72
|
exports.setupCronCleanPendingSessions = setupCronCleanPendingSessions;
|
|
@@ -82,6 +83,8 @@ const async_wait_until_1 = __importDefault(require("async-wait-until"));
|
|
|
82
83
|
const typedi_1 = require("typedi");
|
|
83
84
|
const device_managers_1 = require("./device-managers");
|
|
84
85
|
const device_service_1 = require("./data-service/device-service");
|
|
86
|
+
const deviceClaims_1 = require("./data-service/deviceClaims");
|
|
87
|
+
const PluginContext_1 = require("./PluginContext");
|
|
85
88
|
const logger_1 = __importDefault(require("./logger"));
|
|
86
89
|
const Platform_1 = __importDefault(require("./enums/Platform"));
|
|
87
90
|
const lodash_1 = __importDefault(require("lodash"));
|
|
@@ -91,6 +94,7 @@ const AndroidDeviceManager_1 = __importDefault(require("./device-managers/Androi
|
|
|
91
94
|
const IOSDeviceManager_1 = __importDefault(require("./device-managers/IOSDeviceManager"));
|
|
92
95
|
const IOSDiscoveryService_1 = require("./device-managers/ios/IOSDiscoveryService");
|
|
93
96
|
const NodeDevices_1 = __importDefault(require("./device-managers/NodeDevices"));
|
|
97
|
+
const localDeviceHosts_1 = require("./device-managers/localDeviceHosts");
|
|
94
98
|
const config_1 = require("./config");
|
|
95
99
|
const device_store_1 = require("./data-service/device-store");
|
|
96
100
|
const leaseSessionCaps_1 = require("./services/lease/leaseSessionCaps");
|
|
@@ -204,8 +208,9 @@ function allocateDeviceForSession(capability_1, deviceTimeOutMs_1, deviceQueryIn
|
|
|
204
208
|
const availableCandidate = matchingDevices.find((d) => !(0, device_service_1.isDeviceReserved)(d));
|
|
205
209
|
if (availableCandidate) {
|
|
206
210
|
// Principal Intelligence: Multi-node consistent locking
|
|
207
|
-
// Attempt to atomically claim this specific device
|
|
208
|
-
|
|
211
|
+
// Attempt to atomically claim this specific device: busy, with a
|
|
212
|
+
// pending claim for the session being created (deviceClaims.ts).
|
|
213
|
+
device = yield store.findAndLockDevice(Object.assign(Object.assign({}, filters), { udid: [availableCandidate.udid] }), { claim: true });
|
|
209
214
|
return device !== null;
|
|
210
215
|
}
|
|
211
216
|
logger_1.default.info(`Waiting for free device. Filter: ${JSON.stringify(filters)}}`);
|
|
@@ -245,7 +250,7 @@ function allocateDeviceForSession(capability_1, deviceTimeOutMs_1, deviceQueryIn
|
|
|
245
250
|
const isReady = yield manager.readyForSession(lockedDevice);
|
|
246
251
|
if (!isReady) {
|
|
247
252
|
logger_1.default.error(`❌ [Allocation] Device ${lockedDevice.udid} failed pre-session health check. Unblocking.`);
|
|
248
|
-
yield (0, device_service_1.
|
|
253
|
+
yield (0, device_service_1.releasePendingClaim)(lockedDevice);
|
|
249
254
|
throw new Error(`Device ${lockedDevice.udid} is unhealthy and could not be autonomously recovered.`);
|
|
250
255
|
}
|
|
251
256
|
}
|
|
@@ -468,17 +473,20 @@ function getBusyDevicesCount() {
|
|
|
468
473
|
}).length;
|
|
469
474
|
});
|
|
470
475
|
}
|
|
471
|
-
function updateDeviceList(
|
|
476
|
+
function updateDeviceList(local, hubArgument, tlsRejectUnauthorized) {
|
|
472
477
|
return __awaiter(this, void 0, void 0, function* () {
|
|
473
478
|
const allExistingDevices = yield (0, device_service_1.getAllDevices)();
|
|
474
479
|
const devices = yield getDeviceManager().getDevices(allExistingDevices);
|
|
475
480
|
// first thing first. Update device list in local list
|
|
476
|
-
yield (0, device_service_1.syncDiscoveredDevices)(devices, allExistingDevices,
|
|
477
|
-
// Prune
|
|
478
|
-
//
|
|
479
|
-
//
|
|
481
|
+
yield (0, device_service_1.syncDiscoveredDevices)(devices, allExistingDevices, local.origin);
|
|
482
|
+
// Prune this server's own phones that discovery no longer reports: an
|
|
483
|
+
// unplugged Android phone, or a simulator filtered out (e.g. booted-simulators
|
|
484
|
+
// is on and it shut down). Only its own (isOwnDevice: by nodeId, or else by
|
|
485
|
+
// exact host): a hub keeps its nodes' phones in the same table, and a node
|
|
486
|
+
// on the same machine differs from the hub only by port.
|
|
487
|
+
const nodeId = typedi_1.Container.get(PluginContext_1.PluginContext).nodeId;
|
|
480
488
|
const discoveredUdids = new Set(devices.map((d) => d.udid));
|
|
481
|
-
const staleLocalDevices = allExistingDevices.filter((d) => (
|
|
489
|
+
const staleLocalDevices = allExistingDevices.filter((d) => (0, localDeviceHosts_1.isOwnDevice)(local, nodeId, d) && !discoveredUdids.has(d.udid));
|
|
482
490
|
if (staleLocalDevices.length > 0) {
|
|
483
491
|
logger_1.default.info(`Removing ${staleLocalDevices.length} stale devices/simulators no longer discovered on this host.`);
|
|
484
492
|
yield (0, device_service_1.removeDevice)(staleLocalDevices.map((d) => ({ udid: d.udid, host: d.host })));
|
|
@@ -519,23 +527,25 @@ function refreshSimulatorState(pluginArgs, hostPort) {
|
|
|
519
527
|
}), 10000);
|
|
520
528
|
});
|
|
521
529
|
}
|
|
522
|
-
function setupCronCheckStaleDevices(intervalMs,
|
|
530
|
+
function setupCronCheckStaleDevices(intervalMs, local, tlsRejectUnauthorized) {
|
|
523
531
|
return __awaiter(this, void 0, void 0, function* () {
|
|
524
532
|
setInterval(() => __awaiter(this, void 0, void 0, function* () {
|
|
525
|
-
yield removeStaleDevices(
|
|
533
|
+
yield removeStaleDevices(local, tlsRejectUnauthorized);
|
|
526
534
|
}), intervalMs);
|
|
527
535
|
});
|
|
528
536
|
}
|
|
529
537
|
/**
|
|
530
538
|
* Remove devices where the host is not alive nor defined.
|
|
531
|
-
* @param
|
|
539
|
+
* @param local the hosts this server files its own phones under
|
|
532
540
|
*/
|
|
533
|
-
function removeStaleDevices(
|
|
541
|
+
function removeStaleDevices(local, tlsRejectUnauthorized) {
|
|
534
542
|
return __awaiter(this, void 0, void 0, function* () {
|
|
535
543
|
const allDevices = yield (0, device_service_1.getAllDevices)();
|
|
544
|
+
const nodeId = typedi_1.Container.get(PluginContext_1.PluginContext).nodeId;
|
|
536
545
|
const nodeDevices = allDevices.filter((device) => {
|
|
537
|
-
//
|
|
538
|
-
|
|
546
|
+
// Phones another server drives: by nodeId, or else by exact host (a node
|
|
547
|
+
// on this machine shares the IP, and an IP can be a prefix of another's).
|
|
548
|
+
return device.host !== undefined && !(0, localDeviceHosts_1.isOwnDevice)(local, nodeId, device);
|
|
539
549
|
});
|
|
540
550
|
const devicesWithNoHost = nodeDevices.filter((device) => {
|
|
541
551
|
return device.host === undefined;
|
|
@@ -558,8 +568,14 @@ function removeStaleDevices(currentHost, tlsRejectUnauthorized) {
|
|
|
558
568
|
const allAliveHosts = [...aliveHosts, ...aliveCloudHosts]
|
|
559
569
|
.filter((item) => item.status === 'fulfilled' && item.value.alive)
|
|
560
570
|
.map((item) => item.value.host);
|
|
571
|
+
// A node's phones carry its nodeId, and live as long as the node does:
|
|
572
|
+
// while any host of that node answers, its phone filed under a host that
|
|
573
|
+
// can't be probed (an iPhone under a bare remoteMachineProxyIP) stays.
|
|
574
|
+
const liveNodeIds = new Set(nodeDevices
|
|
575
|
+
.filter((device) => device.nodeId && allAliveHosts.includes(device.host))
|
|
576
|
+
.map((device) => device.nodeId));
|
|
561
577
|
// stale devices are devices that's not alive
|
|
562
|
-
const staleDevices = nodeDevices.filter((device) => !allAliveHosts.includes(device.host));
|
|
578
|
+
const staleDevices = nodeDevices.filter((device) => !allAliveHosts.includes(device.host) && !(device.nodeId && liveNodeIds.has(device.nodeId)));
|
|
563
579
|
yield (0, device_service_1.removeDevice)(staleDevices.map((device) => ({ udid: device.udid, host: device.host })));
|
|
564
580
|
if (staleDevices.length > 0) {
|
|
565
581
|
logger_1.default.debug(`Removing device with udid(s): ${staleDevices
|
|
@@ -586,14 +602,26 @@ function unblockCandidateDevices() {
|
|
|
586
602
|
}
|
|
587
603
|
function releaseBlockedDevices(newCommandTimeout) {
|
|
588
604
|
return __awaiter(this, void 0, void 0, function* () {
|
|
605
|
+
var _a;
|
|
589
606
|
const busyDevices = yield unblockCandidateDevices();
|
|
590
607
|
logger_1.default.debug(`Found ${busyDevices.length} device candidates to be released`);
|
|
591
|
-
|
|
608
|
+
for (const device of busyDevices) {
|
|
592
609
|
// need to keep this to make typescript happy. good thing tho.
|
|
593
610
|
if (device.lastCmdExecutedAt == undefined) {
|
|
594
|
-
|
|
611
|
+
continue;
|
|
595
612
|
}
|
|
596
613
|
const currentEpoch = new Date().getTime();
|
|
614
|
+
// A claim whose session is still being created is not idle: a first
|
|
615
|
+
// driver install can outlast the new-command timeout. It has its own
|
|
616
|
+
// timeout, for a create that never finished (deviceClaims.ts).
|
|
617
|
+
if ((0, deviceClaims_1.isPendingClaim)(device)) {
|
|
618
|
+
if ((0, deviceClaims_1.pendingClaimExpired)(device, currentEpoch)) {
|
|
619
|
+
logger_1.default.warn(`Releasing ${device.udid} at ${device.host}: its session was never created ` +
|
|
620
|
+
`(claimed ${Math.round((currentEpoch - device.claimedAt) / 1000)} seconds ago)`);
|
|
621
|
+
yield (0, device_service_1.releasePendingClaim)(device).catch((err) => logger_1.default.error(`Unable to release ${device.udid}: ${err}`));
|
|
622
|
+
}
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
597
625
|
const timeoutSeconds = device.newCommandTimeout != undefined ? device.newCommandTimeout : newCommandTimeout;
|
|
598
626
|
const timeSinceLastCmdExecuted = (currentEpoch - device.lastCmdExecutedAt) / 1000;
|
|
599
627
|
if (timeSinceLastCmdExecuted > timeoutSeconds) {
|
|
@@ -612,9 +640,14 @@ function releaseBlockedDevices(newCommandTimeout) {
|
|
|
612
640
|
});
|
|
613
641
|
logger_1.default.warn(`🕒 Session ${sessionId} timed out on device ${device.udid}`);
|
|
614
642
|
}
|
|
615
|
-
|
|
643
|
+
// Keyed on the session read here, so a release that loses the race to
|
|
644
|
+
// the session's own end (and the phone's next claim) frees nothing.
|
|
645
|
+
const claimant = (_a = device.claimSessionId) !== null && _a !== void 0 ? _a : device.session_id;
|
|
646
|
+
yield (claimant
|
|
647
|
+
? (0, device_service_1.releaseSessionDevice)(device.udid, device.host, claimant)
|
|
648
|
+
: (0, device_service_1.unblockDevice)(device.udid, device.host)).catch((err) => logger_1.default.error(`Unable to release ${device.udid}: ${err}`));
|
|
616
649
|
}
|
|
617
|
-
}
|
|
650
|
+
}
|
|
618
651
|
});
|
|
619
652
|
}
|
|
620
653
|
function setupCronReleaseBlockedDevices(intervalMs, newCommandTimeoutSec) {
|
|
@@ -628,29 +661,50 @@ function setupCronReleaseBlockedDevices(intervalMs, newCommandTimeoutSec) {
|
|
|
628
661
|
}), intervalMs);
|
|
629
662
|
});
|
|
630
663
|
}
|
|
631
|
-
function setupCronUpdateDeviceList(
|
|
664
|
+
function setupCronUpdateDeviceList(local, hubArgument, intervalMs, tlsRejectUnauthorized) {
|
|
632
665
|
return __awaiter(this, void 0, void 0, function* () {
|
|
633
666
|
if (cronTimerToUpdateDevices) {
|
|
634
667
|
clearInterval(cronTimerToUpdateDevices);
|
|
635
668
|
}
|
|
636
669
|
logger_1.default.info(`This node will send device list update to the hub (${hubArgument}) every ${intervalMs} ms`);
|
|
637
670
|
cronTimerToUpdateDevices = setInterval(() => __awaiter(this, void 0, void 0, function* () {
|
|
638
|
-
yield updateDeviceList(
|
|
671
|
+
yield updateDeviceList(local, hubArgument, tlsRejectUnauthorized);
|
|
639
672
|
}), intervalMs);
|
|
640
673
|
});
|
|
641
674
|
}
|
|
675
|
+
/**
|
|
676
|
+
* Tells the hub this node is leaving, once for each host its phones are filed
|
|
677
|
+
* under, with the node's id: the hub drops the phones this node reported
|
|
678
|
+
* (by `nodeId`, else by exact host) and never another server's. Only URL
|
|
679
|
+
* hosts are sent: a hub before that rule matched anything that isn't a URL as
|
|
680
|
+
* a substring, which is how the node's bare IP also took the hub's own phones
|
|
681
|
+
* on the same machine.
|
|
682
|
+
*/
|
|
683
|
+
function unregisterNodeFromHub(hubArgument, local, tlsRejectUnauthorized, nodeId) {
|
|
684
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
685
|
+
const hub = new NodeDevices_1.default(hubArgument, {
|
|
686
|
+
tlsRejectUnauthorized,
|
|
687
|
+
hubAccessKey: config_1.config.hubAccessKey,
|
|
688
|
+
hubToken: config_1.config.hubToken,
|
|
689
|
+
});
|
|
690
|
+
for (const host of local.hosts) {
|
|
691
|
+
if (/^https?:\/\//.test(host))
|
|
692
|
+
yield hub.unRegisterNode(host, nodeId);
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
}
|
|
642
696
|
/**
|
|
643
697
|
* Principal discovery: Periodically poll for local devices to prune stales/offlines.
|
|
644
698
|
* Critical for standalone Hubs where setupCronUpdateDeviceList isn't called.
|
|
645
699
|
*/
|
|
646
|
-
function setupCronLocalDiscovery(
|
|
700
|
+
function setupCronLocalDiscovery(local, intervalMs) {
|
|
647
701
|
return __awaiter(this, void 0, void 0, function* () {
|
|
648
702
|
if (timer) {
|
|
649
703
|
clearInterval(timer);
|
|
650
704
|
}
|
|
651
705
|
logger_1.default.info(`Local device discovery poll started every ${intervalMs} ms`);
|
|
652
706
|
timer = setInterval(() => __awaiter(this, void 0, void 0, function* () {
|
|
653
|
-
yield updateDeviceList(
|
|
707
|
+
yield updateDeviceList(local);
|
|
654
708
|
}), intervalMs);
|
|
655
709
|
});
|
|
656
710
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CreateHandoff = void 0;
|
|
4
|
+
exports.runWithCreateHandoff = runWithCreateHandoff;
|
|
5
|
+
exports.currentCreateHandoff = currentCreateHandoff;
|
|
6
|
+
const async_hooks_1 = require("async_hooks");
|
|
7
|
+
/**
|
|
8
|
+
* The allocation the session gateway made for one `POST <basePath>/session`,
|
|
9
|
+
* on its way to the plugin.
|
|
10
|
+
*
|
|
11
|
+
* For a phone this server drives, the gateway allocates in front of Appium's
|
|
12
|
+
* route, then calls `next()` inside this request's async context. Appium's
|
|
13
|
+
* route, its umbrella driver and every plugin before Xenon's run inside that
|
|
14
|
+
* context, so XenonPlugin.createSession finds the allocation here and does not
|
|
15
|
+
* allocate a second phone (sessionCreate.ts).
|
|
16
|
+
*
|
|
17
|
+
* It is handed over at most once. Whatever happens first wins:
|
|
18
|
+
* - the plugin takes it, and from then on releasing the phone on failure is
|
|
19
|
+
* the plugin's job (SessionLifecycleService.completeLocalSession);
|
|
20
|
+
* - or the request ends without the plugin taking it (Appium or another
|
|
21
|
+
* plugin refused the create first, or the client went away), and the
|
|
22
|
+
* gateway abandons it, which gives the phone back. A plugin that arrives
|
|
23
|
+
* after that finds it closed.
|
|
24
|
+
*
|
|
25
|
+
* Both happen on the event loop, so there is no interleaving between them.
|
|
26
|
+
* Once taken or given back it keeps no reference to the allocation: things
|
|
27
|
+
* the driver starts during the create (sockets, timers) keep this context,
|
|
28
|
+
* and with it this object, for as long as they live.
|
|
29
|
+
*/
|
|
30
|
+
class CreateHandoff {
|
|
31
|
+
constructor(allocation, giveBack) {
|
|
32
|
+
this.allocation = allocation;
|
|
33
|
+
this.giveBack = giveBack;
|
|
34
|
+
this.state = 'waiting';
|
|
35
|
+
}
|
|
36
|
+
/** The allocation, once. Throws when it was taken already or given back. */
|
|
37
|
+
take() {
|
|
38
|
+
if (this.state === 'taken') {
|
|
39
|
+
throw new Error("This request's device allocation was already used by a createSession.");
|
|
40
|
+
}
|
|
41
|
+
if (this.state === 'closed') {
|
|
42
|
+
throw new Error('The request was closed before its session was created; its device was given back.');
|
|
43
|
+
}
|
|
44
|
+
this.state = 'taken';
|
|
45
|
+
const allocation = this.allocation;
|
|
46
|
+
this.allocation = undefined;
|
|
47
|
+
return allocation;
|
|
48
|
+
}
|
|
49
|
+
/** Give the allocation back if nobody took it. Safe to call more than once. */
|
|
50
|
+
abandon() {
|
|
51
|
+
if (this.state !== 'waiting')
|
|
52
|
+
return;
|
|
53
|
+
this.state = 'closed';
|
|
54
|
+
const allocation = this.allocation;
|
|
55
|
+
this.allocation = undefined;
|
|
56
|
+
try {
|
|
57
|
+
this.giveBack(allocation);
|
|
58
|
+
}
|
|
59
|
+
catch (_a) {
|
|
60
|
+
/* the giver logs its own failures; a throw here would reach the socket */
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
exports.CreateHandoff = CreateHandoff;
|
|
65
|
+
const handoffs = new async_hooks_1.AsyncLocalStorage();
|
|
66
|
+
/** Run `fn` (the rest of the request) with this handoff as its context. */
|
|
67
|
+
function runWithCreateHandoff(handoff, fn) {
|
|
68
|
+
return handoffs.run(handoff, fn);
|
|
69
|
+
}
|
|
70
|
+
/** The handoff of the create request this code runs for, if the gateway made one. */
|
|
71
|
+
function currentCreateHandoff() {
|
|
72
|
+
return handoffs.getStore();
|
|
73
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.hubGatewayOptions = hubGatewayOptions;
|
|
7
|
+
exports.nodeGatewayOptions = nodeGatewayOptions;
|
|
8
|
+
exports.gatewayOptionsFor = gatewayOptionsFor;
|
|
9
|
+
const typedi_1 = require("typedi");
|
|
10
|
+
const base_driver_1 = require("@appium/base-driver");
|
|
11
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
12
|
+
const prisma_1 = require("../prisma");
|
|
13
|
+
const appiumBasePath_1 = require("../app/appiumBasePath");
|
|
14
|
+
const localDeviceHosts_1 = require("../device-managers/localDeviceHosts");
|
|
15
|
+
const SessionManager_1 = require("../sessions/SessionManager");
|
|
16
|
+
const SessionLifecycleService_1 = require("../services/SessionLifecycleService");
|
|
17
|
+
const NetworkConditioningService_1 = require("../services/NetworkConditioningService");
|
|
18
|
+
const event_manager_1 = require("../dashboard/event-manager");
|
|
19
|
+
const device_service_1 = require("../data-service/device-service");
|
|
20
|
+
const hubSessionToken_1 = require("./hubSessionToken");
|
|
21
|
+
const nodeWebDriverUrl_1 = require("./nodeWebDriverUrl");
|
|
22
|
+
const sessionLocator_1 = require("./sessionLocator");
|
|
23
|
+
const sessionGateway_1 = require("./sessionGateway");
|
|
24
|
+
/**
|
|
25
|
+
* The session gateway as a Xenon server runs it: the production wiring of
|
|
26
|
+
* sessionGateway.ts, sessionCreate.ts, sessionLocator.ts and hubSessionToken.ts.
|
|
27
|
+
*/
|
|
28
|
+
const latencyOf = (sessionId) => typedi_1.Container.get(NetworkConditioningService_1.NetworkConditioningService).getLatency(sessionId);
|
|
29
|
+
/** `POST <basePath>/session` in the gateway, with the server's SessionLifecycleService. */
|
|
30
|
+
const createWith = (hubGrants) => ({
|
|
31
|
+
lifecycle: () => typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService),
|
|
32
|
+
hubGrants,
|
|
33
|
+
logger: logger_1.default.scope('SessionGateway'),
|
|
34
|
+
});
|
|
35
|
+
const dashboardHooks = {
|
|
36
|
+
before: (sessionId, command, req, res) => event_manager_1.DASHBORD_EVENT_MANAGER.beforeSessionCommand(sessionId, command, req, res),
|
|
37
|
+
after: (sessionId, command, req, res, body) => event_manager_1.DASHBORD_EVENT_MANAGER.afterSessionCommand(sessionId, command, null, req, res, body),
|
|
38
|
+
};
|
|
39
|
+
/** A hub: sessions other servers run are forwarded to them. */
|
|
40
|
+
function hubGatewayOptions(config) {
|
|
41
|
+
const basePath = (0, appiumBasePath_1.normalizeBasePath)(config.basePath);
|
|
42
|
+
const isLocalDevice = (device) => !device.cloud && (device.nodeId === config.nodeId() || config.isLocalHost(device.host));
|
|
43
|
+
const locator = new sessionLocator_1.SessionLocator(Object.assign(Object.assign({ getSession: (sessionId) => SessionManager_1.SESSION_MANAGER.getSession(sessionId) }, (0, sessionLocator_1.sessionRowsFrom)(prisma_1.prisma)), { isLocalDevice, webDriverUrlOf: (device) => (0, nodeWebDriverUrl_1.nodeWebDriverUrl)(device, basePath) }));
|
|
44
|
+
const tokens = () => typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer);
|
|
45
|
+
const routing = (0, sessionGateway_1.createHubRouting)({
|
|
46
|
+
basePath,
|
|
47
|
+
locate: (sessionId) => locator.locate(sessionId),
|
|
48
|
+
forget: (sessionId) => locator.forget(sessionId),
|
|
49
|
+
tokenFor: (sessionId) => tokens().tokenFor(sessionId),
|
|
50
|
+
forgetToken: (sessionId) => tokens().forget(sessionId),
|
|
51
|
+
deleteSession: (next, sessionId) => typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService).deleteSession(next, sessionId),
|
|
52
|
+
touch: (sessionId) => (0, device_service_1.updateCmdExecutedTime)(sessionId),
|
|
53
|
+
commandName: (path, method) => (0, base_driver_1.routeToCommandName)(path, method, basePath),
|
|
54
|
+
get dashboard() {
|
|
55
|
+
return config.dashboard() ? dashboardHooks : undefined;
|
|
56
|
+
},
|
|
57
|
+
logger: logger_1.default.scope('SessionGateway'),
|
|
58
|
+
});
|
|
59
|
+
return { routing, latencyOf, create: createWith() };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* A node: the hub's session token is accepted in place of the client's
|
|
63
|
+
* credentials, and its create token is a create's credential.
|
|
64
|
+
*/
|
|
65
|
+
function nodeGatewayOptions(hubUrl) {
|
|
66
|
+
const hub = new hubSessionToken_1.HubSessionTokenVerifier(hubUrl);
|
|
67
|
+
return { hubTokens: hub, latencyOf, create: createWith(hub) };
|
|
68
|
+
}
|
|
69
|
+
/** The gateway for this server: a node when it has a hub, a hub otherwise. */
|
|
70
|
+
function gatewayOptionsFor(pluginArgs, cliArgs, nodeId) {
|
|
71
|
+
if (pluginArgs.hub !== undefined)
|
|
72
|
+
return nodeGatewayOptions(pluginArgs.hub);
|
|
73
|
+
const localHosts = (0, localDeviceHosts_1.localDeviceHosts)(pluginArgs, cliArgs.port);
|
|
74
|
+
return hubGatewayOptions({
|
|
75
|
+
basePath: cliArgs.basePath,
|
|
76
|
+
isLocalHost: (host) => (0, localDeviceHosts_1.isLocalDeviceHost)(localHosts, host),
|
|
77
|
+
nodeId,
|
|
78
|
+
dashboard: () => !!pluginArgs.enableDashboard,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.NODE_UNREACHABLE_BODY = exports.NODE_UNREACHABLE_STATUS = void 0;
|
|
7
|
+
exports.forwardedRequestHeaders = forwardedRequestHeaders;
|
|
8
|
+
exports.sendToNode = sendToNode;
|
|
9
|
+
exports.relayAnswer = relayAnswer;
|
|
10
|
+
exports.readAnswer = readAnswer;
|
|
11
|
+
exports.sendAnswer = sendAnswer;
|
|
12
|
+
const http_1 = __importDefault(require("http"));
|
|
13
|
+
const https_1 = __importDefault(require("https"));
|
|
14
|
+
const https_proxy_agent_1 = require("https-proxy-agent");
|
|
15
|
+
const http_proxy_agent_1 = require("http-proxy-agent");
|
|
16
|
+
const internalCall_1 = require("./internalCall");
|
|
17
|
+
const hubSessionToken_1 = require("./hubSessionToken");
|
|
18
|
+
/**
|
|
19
|
+
* Sending a session command on to the node that runs the session, and handing
|
|
20
|
+
* the node's answer back to the client unchanged.
|
|
21
|
+
*/
|
|
22
|
+
// Hop-by-hop headers describe one connection, not the message. `host` and
|
|
23
|
+
// `content-length` are recomputed for the hop to the node.
|
|
24
|
+
const HOP_BY_HOP = new Set([
|
|
25
|
+
'connection',
|
|
26
|
+
'keep-alive',
|
|
27
|
+
'proxy-authenticate',
|
|
28
|
+
'proxy-authorization',
|
|
29
|
+
'proxy-connection',
|
|
30
|
+
'te',
|
|
31
|
+
'trailer',
|
|
32
|
+
'transfer-encoding',
|
|
33
|
+
'upgrade',
|
|
34
|
+
'host',
|
|
35
|
+
'content-length',
|
|
36
|
+
]);
|
|
37
|
+
// The client's credentials were checked here and are not the node's business:
|
|
38
|
+
// the node trusts the hub's session token instead. Xenon's own markers are
|
|
39
|
+
// never taken from a client.
|
|
40
|
+
const NEVER_FORWARDED = new Set([
|
|
41
|
+
'authorization',
|
|
42
|
+
'cookie',
|
|
43
|
+
'x-xenon-access-key',
|
|
44
|
+
'x-xenon-token',
|
|
45
|
+
internalCall_1.INTERNAL_CALL_HEADER,
|
|
46
|
+
hubSessionToken_1.HUB_TOKEN_HEADER,
|
|
47
|
+
]);
|
|
48
|
+
function forwardedRequestHeaders(incoming) {
|
|
49
|
+
const out = {};
|
|
50
|
+
for (const [name, value] of Object.entries(incoming)) {
|
|
51
|
+
const lower = name.toLowerCase();
|
|
52
|
+
if (value === undefined || HOP_BY_HOP.has(lower) || NEVER_FORWARDED.has(lower))
|
|
53
|
+
continue;
|
|
54
|
+
out[lower] = value;
|
|
55
|
+
}
|
|
56
|
+
// The answer is relayed byte for byte and read for the dashboard's command
|
|
57
|
+
// log, so ask for it uncompressed.
|
|
58
|
+
out['accept-encoding'] = 'identity';
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
/** HTTP(S)_PROXY from the environment, as the old command proxy honoured it. */
|
|
62
|
+
function proxyAgentFor(url) {
|
|
63
|
+
const proxy = process.env.HTTP_PROXY || process.env.HTTPS_PROXY;
|
|
64
|
+
if (!proxy)
|
|
65
|
+
return undefined;
|
|
66
|
+
return url.protocol === 'https:' ? new https_proxy_agent_1.HttpsProxyAgent(proxy) : new http_proxy_agent_1.HttpProxyAgent(proxy);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Send one request to a node and resolve with its response stream. No retry:
|
|
70
|
+
* a WebDriver command is not safe to repeat. Credentials in the URL (a cloud
|
|
71
|
+
* provider's) become basic auth.
|
|
72
|
+
*/
|
|
73
|
+
function sendToNode(request) {
|
|
74
|
+
const url = new URL(request.url);
|
|
75
|
+
const lib = url.protocol === 'https:' ? https_1.default : http_1.default;
|
|
76
|
+
const headers = Object.assign({}, request.headers);
|
|
77
|
+
if (request.body)
|
|
78
|
+
headers['content-length'] = request.body.length;
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
const outgoing = lib.request(url, { method: request.method, headers, agent: proxyAgentFor(url), signal: request.signal }, resolve);
|
|
81
|
+
outgoing.on('error', reject);
|
|
82
|
+
outgoing.end(request.body);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function copyResponseHeaders(upstream, res) {
|
|
86
|
+
for (const [name, value] of Object.entries(upstream.headers)) {
|
|
87
|
+
if (value === undefined || HOP_BY_HOP.has(name.toLowerCase()))
|
|
88
|
+
continue;
|
|
89
|
+
res.setHeader(name, value);
|
|
90
|
+
}
|
|
91
|
+
if (upstream.headers['content-length'] !== undefined) {
|
|
92
|
+
res.setHeader('content-length', upstream.headers['content-length']);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Stream the node's answer to the client: its status, its headers (less the
|
|
97
|
+
* hop-by-hop ones) and its body, unchanged. With `capture`, also resolve with
|
|
98
|
+
* the body as text, for the dashboard's command log.
|
|
99
|
+
*/
|
|
100
|
+
function relayAnswer(upstream, res, capture) {
|
|
101
|
+
var _a;
|
|
102
|
+
res.status((_a = upstream.statusCode) !== null && _a !== void 0 ? _a : 502);
|
|
103
|
+
copyResponseHeaders(upstream, res);
|
|
104
|
+
const chunks = capture ? [] : undefined;
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
if (chunks)
|
|
107
|
+
upstream.on('data', (chunk) => chunks.push(chunk));
|
|
108
|
+
upstream.on('end', () => resolve(chunks ? Buffer.concat(chunks).toString('utf8') : undefined));
|
|
109
|
+
upstream.on('error', reject);
|
|
110
|
+
upstream.pipe(res);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/** Read a whole answer, for a caller that must act before replying (DELETE). */
|
|
114
|
+
function readAnswer(upstream) {
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
const chunks = [];
|
|
117
|
+
upstream.on('data', (chunk) => chunks.push(chunk));
|
|
118
|
+
upstream.on('end', () => {
|
|
119
|
+
var _a;
|
|
120
|
+
return resolve({
|
|
121
|
+
status: (_a = upstream.statusCode) !== null && _a !== void 0 ? _a : 502,
|
|
122
|
+
headers: upstream.headers,
|
|
123
|
+
body: Buffer.concat(chunks),
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
upstream.on('error', reject);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function sendAnswer(res, answer) {
|
|
130
|
+
res.status(answer.status);
|
|
131
|
+
for (const [name, value] of Object.entries(answer.headers)) {
|
|
132
|
+
if (value === undefined || HOP_BY_HOP.has(name.toLowerCase()))
|
|
133
|
+
continue;
|
|
134
|
+
res.setHeader(name, value);
|
|
135
|
+
}
|
|
136
|
+
res.setHeader('content-length', answer.body.length);
|
|
137
|
+
res.end(answer.body);
|
|
138
|
+
}
|
|
139
|
+
/** WebDriver's `unknown error`, for a node that could not be reached. */
|
|
140
|
+
exports.NODE_UNREACHABLE_STATUS = 500;
|
|
141
|
+
exports.NODE_UNREACHABLE_BODY = {
|
|
142
|
+
value: {
|
|
143
|
+
error: 'unknown error',
|
|
144
|
+
message: 'Xenon could not reach the node running this session.',
|
|
145
|
+
stacktrace: '',
|
|
146
|
+
},
|
|
147
|
+
};
|