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