@xenon-device-management/xenon 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/lib/package.json +1 -2
- package/lib/public/assets/{AnnotationOverlay-BmTPCAds.js → AnnotationOverlay-C-Pw_hvw.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-B5eB7f8B.js → ApiKeyGate-DmYQ0oSk.js} +1 -1
- package/lib/public/assets/BugReportButton-n8T1dFUp.js +10 -0
- package/lib/public/assets/{DeviceMosaicView-9DUoZnQi.js → DeviceMosaicView-D-rGOBbJ.js} +1 -1
- package/lib/public/assets/{EmptyState-lvyNiXev.js → EmptyState-DExETevp.js} +1 -1
- package/lib/public/assets/{FieldGroup-BduMPM9K.js → FieldGroup-CCdFlZm6.js} +1 -1
- package/lib/public/assets/{FilterMenu-BSqX6I94.js → FilterMenu-DiUSOSL-.js} +1 -1
- package/lib/public/assets/{Menu-Ba5cT7jZ.js → Menu-ZOfpH6gh.js} +1 -1
- package/lib/public/assets/{Modal-CceoMY7D.js → Modal-CZNtoB-1.js} +1 -1
- package/lib/public/assets/{RecordingPage-6T9dOidc.js → RecordingPage-BKp6h8Jl.js} +1 -1
- package/lib/public/assets/{RecordingsPage-qj6FdZXR.js → RecordingsPage-Bf881FX4.js} +1 -1
- package/lib/public/assets/{SegmentedControl-CSRuZ8TR.js → SegmentedControl-EUoyizvW.js} +1 -1
- package/lib/public/assets/{SettingCard-Bp1kz4By.js → SettingCard-ClT4hf4_.js} +1 -1
- package/lib/public/assets/{Table-LV7zFyZg.js → Table-ucR4bnmc.js} +1 -1
- package/lib/public/assets/{activity-D17TUloi.js → activity-K-hdylwq.js} +1 -1
- package/lib/public/assets/{ai-settings-qvbhY89P.js → ai-settings-BLhdwI3c.js} +1 -1
- package/lib/public/assets/{api-keys-BfpvdG7G.js → api-keys-BbiN7rBI.js} +1 -1
- package/lib/public/assets/{apps-Dbnzd0fW.js → apps-Dez1petz.js} +1 -1
- package/lib/public/assets/{arrow-left-DD1VjtC7.js → arrow-left-UjmFHLAJ.js} +1 -1
- package/lib/public/assets/{arrow-right-D0pnM3ma.js → arrow-right-BVyEigbu.js} +1 -1
- package/lib/public/assets/{arrow-up-right-BIC8JOV6.js → arrow-up-right-B-ZlBof3.js} +1 -1
- package/lib/public/assets/{auth-shell-DWHPXSnI.js → auth-shell-DAQjAgUv.js} +2 -2
- package/lib/public/assets/{builds-page-ClCNBUv_.js → builds-page-CmIPa5JK.js} +1 -1
- package/lib/public/assets/{button-xtAhYR-f.js → button-Dd-IhMC1.js} +1 -1
- package/lib/public/assets/{calendar-CupwJ7Bm.js → calendar-CAIeJwa5.js} +1 -1
- package/lib/public/assets/{check-CyuEoKkd.js → check-C4Jrjg8f.js} +1 -1
- package/lib/public/assets/{chevron-left-DgX-F9xK.js → chevron-left-BGzlf-4i.js} +1 -1
- package/lib/public/assets/{chevron-right-BKJgJnU9.js → chevron-right-_wYqDjDC.js} +1 -1
- package/lib/public/assets/{circle-check-CZdg8mMa.js → circle-check-Ckn00yMB.js} +1 -1
- package/lib/public/assets/{circle-x-CZH6t9z-.js → circle-x-pdVkomLa.js} +1 -1
- package/lib/public/assets/{clock-Cd-t7xpe.js → clock-B8vmwkHj.js} +1 -1
- package/lib/public/assets/{copy-C6zerX3I.js → copy-BAqnFI-F.js} +1 -1
- package/lib/public/assets/{device-explorer-B1pI6b_M.js → device-explorer-CYq_4F_N.js} +3 -3
- package/lib/public/assets/device-explorer-D2AqrrLN.css +1 -0
- package/lib/public/assets/{download-x9WP7mcL.js → download-B6UEAp_1.js} +1 -1
- package/lib/public/assets/{forgot-password-B0TEH4N-.js → forgot-password-CLqNvmrN.js} +1 -1
- package/lib/public/assets/{funnel-Cb40MfFN.js → funnel-Da90BBbT.js} +1 -1
- package/lib/public/assets/{index-BR1wTIPC.js → index-BuIIcmGp.js} +2 -2
- package/lib/public/assets/{index-BvH_ekRy.js → index-MX7IzqyW.js} +1 -1
- package/lib/public/assets/{input-DejcCiTI.js → input-Bq9aY-We.js} +1 -1
- package/lib/public/assets/{keycodes-Mavx5iGg.js → keycodes-CIhXIJyP.js} +1 -1
- package/lib/public/assets/{layers-BKqYLiHE.js → layers-bXjpswQI.js} +1 -1
- package/lib/public/assets/{lock-LFsFhc17.js → lock-pJ72y5Fy.js} +1 -1
- package/lib/public/assets/{login-x2pkzt-t.js → login-UOcHALV2.js} +1 -1
- package/lib/public/assets/{maintenance-settings-CYafcoV-.js → maintenance-settings-XnJgp5yb.js} +1 -1
- package/lib/public/assets/{monitor-_7_uJ8d9.js → monitor-QkAhI1Jx.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-BfLuURKN.js → mouse-pointer-2-B42Da0m4.js} +1 -1
- package/lib/public/assets/{network-Z_zBhpsD.js → network-CKVqcyp6.js} +1 -1
- package/lib/public/assets/{overview-BY5uv08S.js → overview-BORGkN3N.js} +1 -1
- package/lib/public/assets/{page-header-CP2UkykG.js → page-header-CKWFOjOI.js} +1 -1
- package/lib/public/assets/{plus-BTD0dnQ0.js → plus-BpFKVdC8.js} +1 -1
- package/lib/public/assets/{profile-page-BhJZuCS7.js → profile-page-CcbKBicY.js} +1 -1
- package/lib/public/assets/{reset-password-CMN0SO81.js → reset-password-BA0W9bxl.js} +1 -1
- package/lib/public/assets/{runbook-page-CBpPX9_5.js → runbook-page-Cp7ZBq3s.js} +1 -1
- package/lib/public/assets/{select-Uc9E5pye.js → select-DIZWIoVR.js} +1 -1
- package/lib/public/assets/{selector-detail-page-BSyBnRuU.js → selector-detail-page-C_4YuvAp.js} +1 -1
- package/lib/public/assets/{selector-health-DzqxNkzm.js → selector-health-CXjD_Xth.js} +1 -1
- package/lib/public/assets/{selector-health-page-CyPLjoD1.js → selector-health-page-CTqIHeoG.js} +1 -1
- package/lib/public/assets/{session-detail-page-BZQZN1P-.js → session-detail-page-B0KYkk8z.js} +1 -1
- package/lib/public/assets/{settings-CkSWiZq8.js → settings-BcRBKUXd.js} +1 -1
- package/lib/public/assets/{status-pill-outline-BFYni2qo.js → status-pill-outline-Dh2PXSf_.js} +1 -1
- package/lib/public/assets/{tablet-BSd-xSZK.js → tablet-_sXqZztt.js} +1 -1
- package/lib/public/assets/{teams-PpoEQz31.js → teams-DGHT8MKm.js} +1 -1
- package/lib/public/assets/{trash-2-BorJv5_u.js → trash-2-BV88cV3Z.js} +1 -1
- package/lib/public/assets/{upload-C3i7sAZl.js → upload-CIDH9wkj.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-CeZQ3EsH.js → use-password-reset-mode-DYGuSNwl.js} +1 -1
- package/lib/public/assets/{users-CEJ3fC78.js → users-7yviqflc.js} +1 -1
- package/lib/public/assets/{users-Drd7ifJx.js → users-BUE7MoTq.js} +1 -1
- package/lib/public/assets/{video-off-CNzn6bPE.js → video-off-BTRxtlGo.js} +1 -1
- package/lib/public/assets/{webhook-settings-BFqX6QXV.js → webhook-settings-DMKD9Pgm.js} +1 -1
- package/lib/public/assets/{zap-4yN8Jdfw.js → zap-CWE94qcK.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 +57 -13
- package/lib/src/app/routers/grid.js +28 -8
- package/lib/src/app/ws/h264StreamWs.js +13 -8
- package/lib/src/app/ws/logcatWs.js +12 -7
- 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 +81 -10
- package/lib/src/data-service/deviceClaims.js +98 -0
- package/lib/src/data-service/deviceFieldOwners.js +32 -10
- package/lib/src/data-service/prisma-store.js +94 -31
- package/lib/src/device-managers/AndroidDeviceManager.js +13 -15
- 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 +3 -3
- package/lib/src/device-managers/android/AndroidStreamService.js +6 -5
- 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 +158 -38
- 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/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 +50 -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/lease/LeaseOrphanSweeper.js +32 -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/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/unit/AndroidDeviceManager.spec.js +8 -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 +6 -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 +18 -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 +28 -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 +291 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +594 -0
- package/lib/test/unit/hub-node-race.spec.js +401 -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 +2 -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 +92 -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 +4 -1
- package/lib/test/unit/loopback-request.spec.js +81 -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/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 +15 -7
- package/lib/test/unit/stream-own-device-row.spec.js +164 -0
- package/lib/test/unit/stream-stop-ownership.spec.js +13 -5
- package/lib/test/unit/stream-stop-recording.spec.js +13 -5
- package/lib/test/unit/stream-ticket-identity.spec.js +15 -5
- 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 +1 -2
- 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
|
@@ -22,6 +22,7 @@ const device_store_1 = require("../../src/data-service/device-store");
|
|
|
22
22
|
const SessionManager_1 = require("../../src/sessions/SessionManager");
|
|
23
23
|
const AndroidStreamService_1 = __importDefault(require("../../src/device-managers/android/AndroidStreamService"));
|
|
24
24
|
const IOSStreamService_1 = __importDefault(require("../../src/device-managers/ios/IOSStreamService"));
|
|
25
|
+
const PluginContext_1 = require("../../src/PluginContext");
|
|
25
26
|
// Named like the real managers: the monitor picks one by constructor.name.
|
|
26
27
|
class AndroidDeviceManager {
|
|
27
28
|
constructor() {
|
|
@@ -33,12 +34,15 @@ class IOSDeviceManager {
|
|
|
33
34
|
this.checkHealth = () => __awaiter(this, void 0, void 0, function* () { return ({ healthStatus: 'Healthy' }); });
|
|
34
35
|
}
|
|
35
36
|
}
|
|
37
|
+
const THIS_SERVER = 'this-server-node-id';
|
|
36
38
|
// A device busy with an Appium session this process no longer knows about,
|
|
37
39
|
// which is the case the monitor reclaims, unless a stream is still running.
|
|
38
40
|
function lostSessionDevice(platform) {
|
|
39
41
|
return {
|
|
40
42
|
udid: `${platform}-1`,
|
|
41
43
|
host: 'http://127.0.0.1:4723',
|
|
44
|
+
// This server's own phone: the monitor checks no other server's.
|
|
45
|
+
nodeId: THIS_SERVER,
|
|
42
46
|
platform,
|
|
43
47
|
busy: true,
|
|
44
48
|
session_id: 'appium-session-not-in-memory',
|
|
@@ -51,7 +55,10 @@ describe('HealthMonitorService — stream lookups', () => {
|
|
|
51
55
|
let androidStatus;
|
|
52
56
|
let iosStatus;
|
|
53
57
|
const reclaimed = () => updateDevice.getCalls().some((c) => { var _a, _b; return ((_a = c.args[2]) === null || _a === void 0 ? void 0 : _a.busy) === false && !((_b = c.args[2]) === null || _b === void 0 ? void 0 : _b.session_id); });
|
|
58
|
+
let nodeIdBefore;
|
|
54
59
|
beforeEach(() => {
|
|
60
|
+
nodeIdBefore = typedi_1.Container.get(PluginContext_1.PluginContext).nodeId;
|
|
61
|
+
typedi_1.Container.get(PluginContext_1.PluginContext).nodeId = THIS_SERVER;
|
|
55
62
|
sinon_1.default.stub(process, 'kill');
|
|
56
63
|
updateDevice = sinon_1.default.spy(() => __awaiter(void 0, void 0, void 0, function* () { return undefined; }));
|
|
57
64
|
sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({
|
|
@@ -78,7 +85,10 @@ describe('HealthMonitorService — stream lookups', () => {
|
|
|
78
85
|
return real(token);
|
|
79
86
|
});
|
|
80
87
|
});
|
|
81
|
-
afterEach(() =>
|
|
88
|
+
afterEach(() => {
|
|
89
|
+
sinon_1.default.restore();
|
|
90
|
+
typedi_1.Container.get(PluginContext_1.PluginContext).nodeId = nodeIdBefore;
|
|
91
|
+
});
|
|
82
92
|
const run = () => new HealthMonitorService_1.HealthMonitorService({}).checkAllDevices();
|
|
83
93
|
it('leaves an Android device with a running stream alone', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
84
94
|
device = lostSessionDevice('android');
|
|
@@ -46,6 +46,33 @@ describe('redactSecrets', () => {
|
|
|
46
46
|
const redacted = (0, helpers_1.redactSecrets)(arr);
|
|
47
47
|
(0, chai_1.expect)(redacted[0]).to.equal('[Circular]');
|
|
48
48
|
});
|
|
49
|
+
// Half of an xe:options credential pair: without its token it proves
|
|
50
|
+
// nothing, but it names the key a leaked token would complete.
|
|
51
|
+
it('should redact access keys in every spelling', () => {
|
|
52
|
+
const KEY = 'xen_AbC123dEf456';
|
|
53
|
+
const obj = {
|
|
54
|
+
accessKey: KEY,
|
|
55
|
+
access_key: KEY,
|
|
56
|
+
ACCESS_KEY: KEY,
|
|
57
|
+
'x-xenon-access-key': KEY,
|
|
58
|
+
'xe:options': { accessKey: KEY, leaseId: 'lse_1' },
|
|
59
|
+
};
|
|
60
|
+
const redacted = (0, helpers_1.redactSecrets)(obj);
|
|
61
|
+
(0, chai_1.expect)(redacted).to.deep.equal({
|
|
62
|
+
accessKey: '***REDACTED***',
|
|
63
|
+
access_key: '***REDACTED***',
|
|
64
|
+
ACCESS_KEY: '***REDACTED***',
|
|
65
|
+
'x-xenon-access-key': '***REDACTED***',
|
|
66
|
+
'xe:options': { accessKey: '***REDACTED***', leaseId: 'lse_1' },
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
// GET /xenon/api/profile/access-key and /auth/me hand the caller their own
|
|
70
|
+
// key. Logging the same object must not take it out of the response.
|
|
71
|
+
it('should leave the object it was given untouched', () => {
|
|
72
|
+
const body = { accessKey: 'xen_AbC123dEf456', nested: { token: 't' } };
|
|
73
|
+
(0, helpers_1.redactSecrets)(body);
|
|
74
|
+
(0, chai_1.expect)(body).to.deep.equal({ accessKey: 'xen_AbC123dEf456', nested: { token: 't' } });
|
|
75
|
+
});
|
|
49
76
|
it('should redact falsy sensitive values', () => {
|
|
50
77
|
const obj = {
|
|
51
78
|
password: '',
|
|
@@ -0,0 +1,192 @@
|
|
|
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
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const os_1 = __importDefault(require("os"));
|
|
18
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
19
|
+
const express_1 = __importDefault(require("express"));
|
|
20
|
+
const typedi_1 = require("typedi");
|
|
21
|
+
const PluginContext_1 = require("../../src/PluginContext");
|
|
22
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
23
|
+
const device_store_1 = require("../../src/data-service/device-store");
|
|
24
|
+
const prisma_store_1 = require("../../src/data-service/prisma-store");
|
|
25
|
+
const SessionManager_1 = require("../../src/sessions/SessionManager");
|
|
26
|
+
const OrphanSweeper_1 = require("../../src/services/OrphanSweeper");
|
|
27
|
+
const SessionHeartbeatService_1 = require("../../src/services/SessionHeartbeatService");
|
|
28
|
+
const nodeWebDriverUrl_1 = require("../../src/gateway/nodeWebDriverUrl");
|
|
29
|
+
const nodeSessionStatus_1 = require("../../src/gateway/nodeSessionStatus");
|
|
30
|
+
const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
|
|
31
|
+
const SocketServer_1 = require("../../src/services/SocketServer");
|
|
32
|
+
const NotificationService_1 = require("../../src/services/NotificationService");
|
|
33
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
34
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
35
|
+
const loopbackServer_1 = require("../helpers/loopbackServer");
|
|
36
|
+
/**
|
|
37
|
+
* A hub restart must not undo the recovery of its nodes' sessions.
|
|
38
|
+
*
|
|
39
|
+
* At boot a hub rebuilds the sessions its nodes still run from their rows
|
|
40
|
+
* (recoverActiveSessions), then sweeps orphans: rows whose last heartbeat is
|
|
41
|
+
* older than 3 heartbeat intervals and was written by another process on
|
|
42
|
+
* this host (OrphanSweeper, at boot and on every interval). A recovered row
|
|
43
|
+
* still carries the old process's heartbeat, so a restart longer than ~90 s
|
|
44
|
+
* failed every node session recovery had just rebuilt and freed its phone,
|
|
45
|
+
* while the node kept the session.
|
|
46
|
+
*
|
|
47
|
+
* A recovered session is this process's from then on, so recovery stamps its
|
|
48
|
+
* heartbeat. A session the node no longer has still ends: the heartbeat asks
|
|
49
|
+
* the node and ends it.
|
|
50
|
+
*/
|
|
51
|
+
describe('a hub restart: the orphan sweep and the sessions recovery rebuilt', () => {
|
|
52
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
53
|
+
const loopback = (0, loopbackServer_1.loopbackServers)();
|
|
54
|
+
const INTERVAL_MS = 30000;
|
|
55
|
+
const OLD_PID = 999999;
|
|
56
|
+
const TEN_MINUTES_AGO = () => new Date(Date.now() - 10 * 60000);
|
|
57
|
+
let context;
|
|
58
|
+
let saved;
|
|
59
|
+
let restore;
|
|
60
|
+
let nodeOrigin;
|
|
61
|
+
let nodeHas;
|
|
62
|
+
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
63
|
+
nodeHas = new Set();
|
|
64
|
+
const node = (0, express_1.default)();
|
|
65
|
+
node.get('/xenon/api/webdriver', (0, nodeWebDriverUrl_1.webdriverInfoHandler)(() => ''));
|
|
66
|
+
node.get('/xenon/api/node/sessions/:sessionId', (req, res) => {
|
|
67
|
+
res.setHeader(nodeSessionStatus_1.NODE_SESSION_STATUS_HEADER, '1');
|
|
68
|
+
const { sessionId } = req.params;
|
|
69
|
+
res.json({ value: { sessionId, exists: nodeHas.has(sessionId) } });
|
|
70
|
+
});
|
|
71
|
+
const server = yield loopback.serve(node);
|
|
72
|
+
nodeOrigin = `http://127.0.0.1:${server.address().port}`;
|
|
73
|
+
restore = (0, container_registration_1.saveRegistrations)(nodeWebDriverUrl_1.NodeBasePathResolver, nodeSessionStatus_1.NodeSessionProbeSupport, hubSessionToken_1.HubSessionTokenIssuer, SocketServer_1.SocketServer, NotificationService_1.NotificationService);
|
|
74
|
+
typedi_1.Container.set(nodeWebDriverUrl_1.NodeBasePathResolver, new nodeWebDriverUrl_1.NodeBasePathResolver());
|
|
75
|
+
typedi_1.Container.set(nodeSessionStatus_1.NodeSessionProbeSupport, new nodeSessionStatus_1.NodeSessionProbeSupport());
|
|
76
|
+
const issuer = new hubSessionToken_1.HubSessionTokenIssuer();
|
|
77
|
+
issuer.useSigner({ sign: () => __awaiter(void 0, void 0, void 0, function* () { return 'hub-token'; }) });
|
|
78
|
+
typedi_1.Container.set(hubSessionToken_1.HubSessionTokenIssuer, issuer);
|
|
79
|
+
typedi_1.Container.set({
|
|
80
|
+
id: SocketServer_1.SocketServer,
|
|
81
|
+
value: {
|
|
82
|
+
emitToDashboard: () => undefined,
|
|
83
|
+
emitToDashboardForDevices: () => __awaiter(void 0, void 0, void 0, function* () { return undefined; }),
|
|
84
|
+
hasScopedDashboard: () => false,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
typedi_1.Container.set({ id: NotificationService_1.NotificationService, value: { dispatchEvent: () => __awaiter(void 0, void 0, void 0, function* () { return undefined; }) } });
|
|
88
|
+
context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
89
|
+
saved = { context: Object.assign({}, context), store: device_store_1.DeviceStoreFactory._deviceStore };
|
|
90
|
+
device_store_1.DeviceStoreFactory._deviceStore = new prisma_store_1.PrismaDeviceStore();
|
|
91
|
+
context.setContext(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), 4799, 'hub-after-restart', '');
|
|
92
|
+
yield scratch.db.session.deleteMany({});
|
|
93
|
+
yield scratch.db.device.deleteMany({});
|
|
94
|
+
}));
|
|
95
|
+
afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
96
|
+
for (const s of SessionManager_1.SESSION_MANAGER.getAllSessions())
|
|
97
|
+
SessionManager_1.SESSION_MANAGER.removeSession(s.getId());
|
|
98
|
+
Object.assign(context, saved.context);
|
|
99
|
+
device_store_1.DeviceStoreFactory._deviceStore = saved.store;
|
|
100
|
+
sinon_1.default.restore();
|
|
101
|
+
restore();
|
|
102
|
+
yield loopback.closeAll();
|
|
103
|
+
}));
|
|
104
|
+
/** A node session the hub ran before it went down, last heartbeat 10 minutes ago. */
|
|
105
|
+
function nodeSessionFromBeforeTheRestart(sessionId, udid) {
|
|
106
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
107
|
+
yield scratch.db.device.create({
|
|
108
|
+
data: {
|
|
109
|
+
udid,
|
|
110
|
+
host: nodeOrigin,
|
|
111
|
+
nodeId: 'node-peer',
|
|
112
|
+
platform: 'android',
|
|
113
|
+
name: udid,
|
|
114
|
+
busy: true,
|
|
115
|
+
session_id: sessionId,
|
|
116
|
+
claimSessionId: sessionId,
|
|
117
|
+
claimedAt: Date.now() - 11 * 60000,
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
yield staleRow(sessionId, udid, 'node-peer');
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
function staleRow(sessionId, udid, nodeId) {
|
|
124
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
125
|
+
yield scratch.db.session.create({
|
|
126
|
+
data: {
|
|
127
|
+
id: sessionId,
|
|
128
|
+
desired_capabilities: '{}',
|
|
129
|
+
session_capabilities: '{}',
|
|
130
|
+
node_id: nodeId,
|
|
131
|
+
has_live_video: false,
|
|
132
|
+
device_udid: udid,
|
|
133
|
+
device_platform: 'android',
|
|
134
|
+
device_version: '14',
|
|
135
|
+
status: 'running',
|
|
136
|
+
user_id: 'alice',
|
|
137
|
+
heartbeat_host: os_1.default.hostname(),
|
|
138
|
+
heartbeat_pid: OLD_PID,
|
|
139
|
+
last_heartbeat_at: TEN_MINUTES_AGO(),
|
|
140
|
+
updatedAt: TEN_MINUTES_AGO(),
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const status = (id) => __awaiter(void 0, void 0, void 0, function* () { var _a; return (_a = (yield scratch.db.session.findUnique({ where: { id } }))) === null || _a === void 0 ? void 0 : _a.status; });
|
|
146
|
+
const phone = (udid) => scratch.db.device.findFirst({ where: { udid } });
|
|
147
|
+
const bootSweep = () => typedi_1.Container.get(OrphanSweeper_1.OrphanSweeper).sweep({
|
|
148
|
+
heartbeatIntervalMs: INTERVAL_MS,
|
|
149
|
+
hostScope: { host: os_1.default.hostname(), excludePid: process.pid },
|
|
150
|
+
});
|
|
151
|
+
const periodicSweep = () => typedi_1.Container.get(OrphanSweeper_1.OrphanSweeper).sweep({ heartbeatIntervalMs: INTERVAL_MS });
|
|
152
|
+
it('a recovered session with a 10-minute-old heartbeat survives the boot sweep', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
153
|
+
yield nodeSessionFromBeforeTheRestart('node-session', 'node-phone');
|
|
154
|
+
nodeHas.add('node-session');
|
|
155
|
+
(0, chai_1.expect)(yield SessionManager_1.SESSION_MANAGER.recoverActiveSessions('hub-after-restart', '')).to.equal(1);
|
|
156
|
+
yield bootSweep();
|
|
157
|
+
(0, chai_1.expect)(yield status('node-session')).to.equal('running');
|
|
158
|
+
(0, chai_1.expect)(SessionManager_1.SESSION_MANAGER.getSession('node-session'), 'still routed').to.not.equal(undefined);
|
|
159
|
+
(0, chai_1.expect)(yield phone('node-phone')).to.include({ busy: true, claimSessionId: 'node-session' });
|
|
160
|
+
}));
|
|
161
|
+
it('and the periodic sweep that follows', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
162
|
+
yield nodeSessionFromBeforeTheRestart('node-session', 'node-phone');
|
|
163
|
+
nodeHas.add('node-session');
|
|
164
|
+
yield SessionManager_1.SESSION_MANAGER.recoverActiveSessions('hub-after-restart', '');
|
|
165
|
+
yield bootSweep();
|
|
166
|
+
yield periodicSweep();
|
|
167
|
+
(0, chai_1.expect)(yield status('node-session')).to.equal('running');
|
|
168
|
+
}));
|
|
169
|
+
it('a stale session on this host that recovery did not rebuild is still swept', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
170
|
+
yield nodeSessionFromBeforeTheRestart('node-session', 'node-phone');
|
|
171
|
+
nodeHas.add('node-session');
|
|
172
|
+
yield SessionManager_1.SESSION_MANAGER.recoverActiveSessions('hub-after-restart', '');
|
|
173
|
+
// Not in the table recovery read: nothing rebuilt it, nothing heartbeats it.
|
|
174
|
+
yield staleRow('orphan-session', 'gone-phone', 'node-peer');
|
|
175
|
+
yield bootSweep();
|
|
176
|
+
(0, chai_1.expect)(yield status('orphan-session')).to.equal('failed');
|
|
177
|
+
(0, chai_1.expect)(yield status('node-session')).to.equal('running');
|
|
178
|
+
}));
|
|
179
|
+
it('a recovered session the node no longer has is ended by the heartbeat', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
180
|
+
yield nodeSessionFromBeforeTheRestart('node-session', 'node-phone');
|
|
181
|
+
yield SessionManager_1.SESSION_MANAGER.recoverActiveSessions('hub-after-restart', '');
|
|
182
|
+
yield bootSweep();
|
|
183
|
+
(0, chai_1.expect)(yield status('node-session')).to.equal('running');
|
|
184
|
+
// The node says it has no such session (nodeHas is empty).
|
|
185
|
+
const heartbeat = new SessionHeartbeatService_1.SessionHeartbeatService();
|
|
186
|
+
for (let i = 0; i < 6; i++)
|
|
187
|
+
yield heartbeat.checkAllSessions();
|
|
188
|
+
(0, chai_1.expect)(yield status('node-session')).to.equal('failed');
|
|
189
|
+
(0, chai_1.expect)(SessionManager_1.SESSION_MANAGER.getSession('node-session')).to.equal(undefined);
|
|
190
|
+
(0, chai_1.expect)(yield phone('node-phone')).to.include({ busy: false, claimSessionId: null });
|
|
191
|
+
}));
|
|
192
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
38
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
39
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
40
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
41
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
45
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
|
+
};
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
require("reflect-metadata");
|
|
49
|
+
const chai_1 = require("chai");
|
|
50
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
51
|
+
const fs_1 = __importDefault(require("fs"));
|
|
52
|
+
const os_1 = __importDefault(require("os"));
|
|
53
|
+
const path_1 = __importDefault(require("path"));
|
|
54
|
+
const express_1 = __importDefault(require("express"));
|
|
55
|
+
const jose = __importStar(require("jose"));
|
|
56
|
+
const typedi_1 = require("typedi");
|
|
57
|
+
const SessionLifecycleService_1 = require("../../src/services/SessionLifecycleService");
|
|
58
|
+
const PluginContext_1 = require("../../src/PluginContext");
|
|
59
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
60
|
+
const nodeWebDriverUrl_1 = require("../../src/gateway/nodeWebDriverUrl");
|
|
61
|
+
const JwtKeyService_1 = require("../../src/services/token/JwtKeyService");
|
|
62
|
+
const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
|
|
63
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
64
|
+
const loopbackServer_1 = require("../helpers/loopbackServer");
|
|
65
|
+
/**
|
|
66
|
+
* A create the hub forwards to a node runs once.
|
|
67
|
+
*
|
|
68
|
+
* InternalHttpClient retries a request on a 5xx or on its 30 s timeout. A
|
|
69
|
+
* create is not safe to repeat: a first UiAutomator2 install easily outlasts
|
|
70
|
+
* 30 s, and every retry started another session on the node, holding the phone
|
|
71
|
+
* with nobody to end it. So the hub sends a create once, with a timeout sized
|
|
72
|
+
* for a create, and the node takes the hub's create token only once.
|
|
73
|
+
*/
|
|
74
|
+
describe('a create the hub forwards runs once', () => {
|
|
75
|
+
const loopback = (0, loopbackServer_1.loopbackServers)();
|
|
76
|
+
let context;
|
|
77
|
+
let saved;
|
|
78
|
+
let restore;
|
|
79
|
+
let timeoutBefore;
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
restore = (0, container_registration_1.saveRegistrations)(nodeWebDriverUrl_1.NodeBasePathResolver);
|
|
82
|
+
typedi_1.Container.set(nodeWebDriverUrl_1.NodeBasePathResolver, new nodeWebDriverUrl_1.NodeBasePathResolver());
|
|
83
|
+
context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
84
|
+
saved = Object.assign({}, context);
|
|
85
|
+
// A client of its own, built now: one that reads XENON_HTTP_TIMEOUT_MS below.
|
|
86
|
+
context.pluginArgs = Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { tlsRejectUnauthorized: true });
|
|
87
|
+
context.nodeId = 'hub-node-id';
|
|
88
|
+
context.nodeBasePath = '';
|
|
89
|
+
timeoutBefore = process.env.XENON_HTTP_TIMEOUT_MS;
|
|
90
|
+
});
|
|
91
|
+
afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
92
|
+
Object.assign(context, saved);
|
|
93
|
+
if (timeoutBefore === undefined)
|
|
94
|
+
delete process.env.XENON_HTTP_TIMEOUT_MS;
|
|
95
|
+
else
|
|
96
|
+
process.env.XENON_HTTP_TIMEOUT_MS = timeoutBefore;
|
|
97
|
+
sinon_1.default.restore();
|
|
98
|
+
restore();
|
|
99
|
+
yield loopback.closeAll();
|
|
100
|
+
}));
|
|
101
|
+
/** A node whose create answers with `answer` for each POST, in order. */
|
|
102
|
+
function node(answers) {
|
|
103
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
104
|
+
const creates = [];
|
|
105
|
+
const app = (0, express_1.default)();
|
|
106
|
+
app.use(express_1.default.json());
|
|
107
|
+
app.get('/xenon/api/webdriver', (0, nodeWebDriverUrl_1.webdriverInfoHandler)(() => ''));
|
|
108
|
+
app.post('/session', (_req, res) => {
|
|
109
|
+
var _a;
|
|
110
|
+
const n = creates.length;
|
|
111
|
+
creates.push(Date.now());
|
|
112
|
+
const answer = answers[Math.min(n, answers.length - 1)];
|
|
113
|
+
setTimeout(() => {
|
|
114
|
+
if (answer.status === 200) {
|
|
115
|
+
res.json({ value: { sessionId: `s-${n + 1}`, capabilities: {} } });
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
res.status(answer.status).json({
|
|
119
|
+
value: { error: 'unknown error', message: 'driver install failed', stacktrace: '' },
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}, (_a = answer.delayMs) !== null && _a !== void 0 ? _a : 0);
|
|
123
|
+
});
|
|
124
|
+
const server = yield loopback.serve(app);
|
|
125
|
+
const origin = `http://127.0.0.1:${server.address().port}`;
|
|
126
|
+
const device = { udid: 'u1', host: origin, nodeId: 'node-peer', platform: 'android' };
|
|
127
|
+
return { creates, device };
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const caps = { alwaysMatch: { platformName: 'Android' }, firstMatch: [{}] };
|
|
131
|
+
it('is not sent again when the node answers a 5xx', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
132
|
+
const { creates, device } = yield node([{ status: 500 }, { status: 200 }]);
|
|
133
|
+
const answer = yield new SessionLifecycleService_1.SessionLifecycleService().forwardSessionRequest(device, caps);
|
|
134
|
+
(0, chai_1.expect)(creates.length, 'creates the node received').to.equal(1);
|
|
135
|
+
(0, chai_1.expect)(answer).to.be.instanceOf(Error);
|
|
136
|
+
}));
|
|
137
|
+
it('waits for a create that outlasts the ordinary request timeout, and sends it once', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
138
|
+
var _a, _b;
|
|
139
|
+
process.env.XENON_HTTP_TIMEOUT_MS = '150';
|
|
140
|
+
const { creates, device } = yield node([{ status: 200, delayMs: 600 }]);
|
|
141
|
+
const answer = yield new SessionLifecycleService_1.SessionLifecycleService().forwardSessionRequest(device, caps);
|
|
142
|
+
(0, chai_1.expect)(creates.length, 'creates the node received').to.equal(1);
|
|
143
|
+
(0, chai_1.expect)((_a = answer.value) === null || _a === void 0 ? void 0 : _a[0], JSON.stringify((_b = answer === null || answer === void 0 ? void 0 : answer.message) !== null && _b !== void 0 ? _b : answer)).to.equal('s-1');
|
|
144
|
+
}));
|
|
145
|
+
});
|
|
146
|
+
describe('the hub’s create token is taken once', () => {
|
|
147
|
+
const loopback = (0, loopbackServer_1.loopbackServers)();
|
|
148
|
+
let dirs;
|
|
149
|
+
let restore;
|
|
150
|
+
let hubKeys;
|
|
151
|
+
let hubUrl;
|
|
152
|
+
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
153
|
+
dirs = [fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-create-once-'))];
|
|
154
|
+
restore = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, hubSessionToken_1.HubSessionTokenIssuer);
|
|
155
|
+
hubKeys = new JwtKeyService_1.JwtKeyService();
|
|
156
|
+
yield hubKeys.init(dirs[0]);
|
|
157
|
+
typedi_1.Container.set(JwtKeyService_1.JwtKeyService, hubKeys);
|
|
158
|
+
typedi_1.Container.set(hubSessionToken_1.HubSessionTokenIssuer, new hubSessionToken_1.HubSessionTokenIssuer());
|
|
159
|
+
const hub = (0, express_1.default)();
|
|
160
|
+
hub.get('/xenon/api/auth/jwks.json', (_req, res) => res.json(hubKeys.jwks()));
|
|
161
|
+
const server = yield loopback.serve(hub);
|
|
162
|
+
hubUrl = `http://127.0.0.1:${server.address().port}`;
|
|
163
|
+
}));
|
|
164
|
+
afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
165
|
+
sinon_1.default.restore();
|
|
166
|
+
restore();
|
|
167
|
+
yield loopback.closeAll();
|
|
168
|
+
for (const dir of dirs)
|
|
169
|
+
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
170
|
+
}));
|
|
171
|
+
const grant = { userId: 'alice', udid: 'phone-1', host: 'http://10.0.0.5:4725' };
|
|
172
|
+
it('each create’s token has an id of its own', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
173
|
+
const issuer = typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer);
|
|
174
|
+
const a = jose.decodeJwt((yield issuer.createTokenFor(grant)));
|
|
175
|
+
const b = jose.decodeJwt((yield issuer.createTokenFor(grant)));
|
|
176
|
+
(0, chai_1.expect)(a.jti).to.be.a('string').and.not.equal('');
|
|
177
|
+
(0, chai_1.expect)(b.jti).to.be.a('string').and.not.equal(a.jti);
|
|
178
|
+
}));
|
|
179
|
+
it('the node accepts it once and refuses it after that', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
180
|
+
const token = (yield typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer).createTokenFor(grant));
|
|
181
|
+
const node = new hubSessionToken_1.HubSessionTokenVerifier(hubUrl);
|
|
182
|
+
(0, chai_1.expect)(yield node.verifyCreate(token)).to.deep.equal(grant);
|
|
183
|
+
(0, chai_1.expect)(yield node.verifyCreate(token), 'the same token again').to.equal(null);
|
|
184
|
+
}));
|
|
185
|
+
it('a token from a hub that sets no id (an older hub) is still accepted', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
186
|
+
const token = yield hubKeys.sign({ sub: 'alice', udid: 'phone-1', host: 'http://10.0.0.5:4725' }, { audience: hubSessionToken_1.HUB_CREATE_AUDIENCE, ttlSeconds: 120 });
|
|
187
|
+
const node = new hubSessionToken_1.HubSessionTokenVerifier(hubUrl);
|
|
188
|
+
(0, chai_1.expect)(yield node.verifyCreate(token)).to.deep.equal(grant);
|
|
189
|
+
}));
|
|
190
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
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
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const http_1 = __importDefault(require("http"));
|
|
18
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
19
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
20
|
+
const typedi_1 = require("typedi");
|
|
21
|
+
const plugin_1 = require("../../src/plugin");
|
|
22
|
+
const logger_1 = __importDefault(require("../../src/logger"));
|
|
23
|
+
const SessionLifecycleService_1 = require("../../src/services/SessionLifecycleService");
|
|
24
|
+
const registerCommandAuth_1 = require("../../src/app/registerCommandAuth");
|
|
25
|
+
const appium_umbrella_1 = require("../helpers/appium-umbrella");
|
|
26
|
+
/**
|
|
27
|
+
* createSession on a node's phone, against Appium's real umbrella driver
|
|
28
|
+
* (base-driver 10.2.0, the one installed here).
|
|
29
|
+
*
|
|
30
|
+
* The umbrella can't hold a session another server runs. If Xenon's plugin
|
|
31
|
+
* answers such a createSession itself, without calling next(), the umbrella
|
|
32
|
+
* never records the session, then promotes its sessionless plugins to that id
|
|
33
|
+
* and builds their log prefix from `this.sessions[id]`, which is undefined:
|
|
34
|
+
* `generateDriverLogPrefix(undefined)` throws and the client gets a 500 while
|
|
35
|
+
* the node keeps the session (Appium 2.15 up to base-driver 10.2.0). Later
|
|
36
|
+
* base-drivers don't throw, but still leave the promoted plugin behind.
|
|
37
|
+
*
|
|
38
|
+
* So the session gateway creates it, in front of Appium's route, and answers
|
|
39
|
+
* it there: the umbrella never sees it. The guard below keeps the old failure
|
|
40
|
+
* reproducible, so the gateway is shown to be what prevents it.
|
|
41
|
+
*/
|
|
42
|
+
describe('createSession on a node’s phone, in Appium’s real umbrella driver', function () {
|
|
43
|
+
this.timeout(60000);
|
|
44
|
+
const quiet = { info: () => undefined, warn: () => undefined, error: () => undefined };
|
|
45
|
+
let server;
|
|
46
|
+
let umbrella;
|
|
47
|
+
let appiumLogs;
|
|
48
|
+
before(() => {
|
|
49
|
+
appiumLogs = (0, appium_umbrella_1.quietAppiumLogs)();
|
|
50
|
+
});
|
|
51
|
+
after(() => appiumLogs.restore());
|
|
52
|
+
afterEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
53
|
+
sinon_1.default.restore();
|
|
54
|
+
if (!server)
|
|
55
|
+
return;
|
|
56
|
+
const s = server;
|
|
57
|
+
server = undefined;
|
|
58
|
+
s.closeAllConnections();
|
|
59
|
+
yield new Promise((resolve) => http_1.default.Server.prototype.close.call(s, () => resolve()));
|
|
60
|
+
}));
|
|
61
|
+
function boot(opts) {
|
|
62
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
63
|
+
const baseDriver = (0, appium_umbrella_1.appiumBaseDriver)();
|
|
64
|
+
umbrella = (0, appium_umbrella_1.umbrellaWith)([[plugin_1.XenonPlugin, 'xenon']]);
|
|
65
|
+
server = yield baseDriver.server({
|
|
66
|
+
routeConfiguringFunction: baseDriver.routeConfiguringFunction(umbrella),
|
|
67
|
+
port: 0,
|
|
68
|
+
hostname: '127.0.0.1',
|
|
69
|
+
basePath: '/wd/hub',
|
|
70
|
+
cliArgs: { basePath: '/wd/hub' },
|
|
71
|
+
serverUpdaters: opts.gateway
|
|
72
|
+
? [
|
|
73
|
+
(app) => (0, registerCommandAuth_1.registerSessionGateway)(app, { basePath: '/wd/hub' }, (0, registerCommandAuth_1.commandAuthDeps)({ enabled: () => false, authDisabled: () => false, logger: quiet }), { create: { lifecycle: () => typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService) } }),
|
|
74
|
+
]
|
|
75
|
+
: [],
|
|
76
|
+
});
|
|
77
|
+
return server;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const caps = {
|
|
81
|
+
capabilities: {
|
|
82
|
+
alwaysMatch: { platformName: 'Android', 'appium:automationName': appium_umbrella_1.FAKE_AUTOMATION },
|
|
83
|
+
firstMatch: [{}],
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
const remoteAllocation = { remote: true, device: { udid: 'node-phone' } };
|
|
87
|
+
const nodeSession = {
|
|
88
|
+
protocol: 'W3C',
|
|
89
|
+
value: ['node-session-1', { platformName: 'Android' }, 'W3C'],
|
|
90
|
+
};
|
|
91
|
+
it('through the gateway, never reaches Appium’s createSession and leaves no plugin behind', () => __awaiter(this, void 0, void 0, function* () {
|
|
92
|
+
sinon_1.default.stub(SessionLifecycleService_1.SessionLifecycleService.prototype, 'prepareSession').resolves(remoteAllocation);
|
|
93
|
+
sinon_1.default.stub(SessionLifecycleService_1.SessionLifecycleService.prototype, 'completeRemoteSession').resolves(nodeSession);
|
|
94
|
+
const app = yield boot({ gateway: true });
|
|
95
|
+
const umbrellaCommands = sinon_1.default.spy(umbrella, 'executeCommand');
|
|
96
|
+
const pluginCreate = sinon_1.default.spy(plugin_1.XenonPlugin.prototype, 'createSession');
|
|
97
|
+
const res = yield (0, loopbackRequest_1.default)(app).post('/wd/hub/session').send(caps);
|
|
98
|
+
(0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
|
|
99
|
+
(0, chai_1.expect)(res.body).to.deep.equal({
|
|
100
|
+
value: { capabilities: { platformName: 'Android' }, sessionId: 'node-session-1' },
|
|
101
|
+
});
|
|
102
|
+
(0, chai_1.expect)(umbrellaCommands.called, 'the umbrella').to.equal(false);
|
|
103
|
+
(0, chai_1.expect)(pluginCreate.called, 'the plugin').to.equal(false);
|
|
104
|
+
(0, chai_1.expect)(umbrella.sessions).to.deep.equal({});
|
|
105
|
+
(0, chai_1.expect)(umbrella.sessionlessPlugins).to.deep.equal([]);
|
|
106
|
+
(0, chai_1.expect)(umbrella.sessionPlugins).to.deep.equal({});
|
|
107
|
+
}));
|
|
108
|
+
it('guard: without the gateway, a plugin that answers it itself still gets Appium’s 500', () => __awaiter(this, void 0, void 0, function* () {
|
|
109
|
+
// What the plugin did before the gateway: create on the node, never call next().
|
|
110
|
+
sinon_1.default.stub(SessionLifecycleService_1.SessionLifecycleService.prototype, 'createSession').resolves(nodeSession);
|
|
111
|
+
const app = yield boot({ gateway: false });
|
|
112
|
+
const res = yield (0, loopbackRequest_1.default)(app).post('/wd/hub/session').send(caps);
|
|
113
|
+
(0, chai_1.expect)(res.status).to.equal(500);
|
|
114
|
+
(0, chai_1.expect)(res.body.value.message).to.include("Cannot read properties of undefined (reading 'constructor')");
|
|
115
|
+
}));
|
|
116
|
+
it('the plugin no longer drops its log-prefix hook (the stopgap is gone)', () => __awaiter(this, void 0, void 0, function* () {
|
|
117
|
+
sinon_1.default.stub(SessionLifecycleService_1.SessionLifecycleService.prototype, 'createSession').resolves(nodeSession);
|
|
118
|
+
const plugin = new plugin_1.XenonPlugin('xenon', {});
|
|
119
|
+
yield plugin.createSession(sinon_1.default.stub(), {}, {}, {}, caps.capabilities);
|
|
120
|
+
(0, chai_1.expect)(plugin.updateLogPrefix).to.be.a('function');
|
|
121
|
+
}));
|
|
122
|
+
describe('when a create reaches the plugin without the gateway', () => {
|
|
123
|
+
it('a node’s phone is refused and given back, never answered for Appium', () => __awaiter(this, void 0, void 0, function* () {
|
|
124
|
+
sinon_1.default.stub(SessionLifecycleService_1.SessionLifecycleService.prototype, 'prepareSession').resolves(remoteAllocation);
|
|
125
|
+
const forward = sinon_1.default.stub(SessionLifecycleService_1.SessionLifecycleService.prototype, 'completeRemoteSession');
|
|
126
|
+
const release = sinon_1.default.stub(SessionLifecycleService_1.SessionLifecycleService.prototype, 'releaseAllocation').resolves();
|
|
127
|
+
const app = yield boot({ gateway: false });
|
|
128
|
+
const res = yield (0, loopbackRequest_1.default)(app).post('/wd/hub/session').send(caps);
|
|
129
|
+
(0, chai_1.expect)(res.status).to.equal(500);
|
|
130
|
+
(0, chai_1.expect)(res.body.value.error).to.equal('session not created');
|
|
131
|
+
(0, chai_1.expect)(res.body.value.message).to.include('session gateway');
|
|
132
|
+
(0, chai_1.expect)(forward.called, 'forwarded to the node').to.equal(false);
|
|
133
|
+
(0, chai_1.expect)(release.calledOnceWith(remoteAllocation)).to.equal(true);
|
|
134
|
+
}));
|
|
135
|
+
it('a local phone is still created, by the plugin, and says so', () => __awaiter(this, void 0, void 0, function* () {
|
|
136
|
+
sinon_1.default
|
|
137
|
+
.stub(SessionLifecycleService_1.SessionLifecycleService.prototype, 'prepareSession')
|
|
138
|
+
.resolves({ remote: false, device: { udid: 'hub-phone' } });
|
|
139
|
+
sinon_1.default
|
|
140
|
+
.stub(SessionLifecycleService_1.SessionLifecycleService.prototype, 'completeLocalSession')
|
|
141
|
+
.callsFake((_allocation, next) => __awaiter(this, void 0, void 0, function* () { return next(); }));
|
|
142
|
+
// The umbrella makes the plugin instance on this request, with this logger.
|
|
143
|
+
const warnings = [];
|
|
144
|
+
const scope = logger_1.default.scope.bind(logger_1.default);
|
|
145
|
+
sinon_1.default.stub(logger_1.default, 'scope').callsFake((name) => {
|
|
146
|
+
const scoped = scope(name);
|
|
147
|
+
if (name !== 'Plugin')
|
|
148
|
+
return scoped;
|
|
149
|
+
return Object.assign(Object.create(scoped), {
|
|
150
|
+
warn: (message) => warnings.push(message),
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
const app = yield boot({ gateway: false });
|
|
154
|
+
const res = yield (0, loopbackRequest_1.default)(app).post('/wd/hub/session').send(caps);
|
|
155
|
+
(0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
|
|
156
|
+
(0, chai_1.expect)(Object.keys(umbrella.sessions)).to.deep.equal([res.body.value.sessionId]);
|
|
157
|
+
(0, chai_1.expect)(warnings.join('\n')).to.include('session gateway');
|
|
158
|
+
}));
|
|
159
|
+
});
|
|
160
|
+
});
|