@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
|
@@ -0,0 +1,299 @@
|
|
|
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 child_process_1 = __importDefault(require("child_process"));
|
|
18
|
+
const events_1 = require("events");
|
|
19
|
+
const fs_1 = __importDefault(require("fs"));
|
|
20
|
+
const net_1 = __importDefault(require("net"));
|
|
21
|
+
const os_1 = __importDefault(require("os"));
|
|
22
|
+
const path_1 = __importDefault(require("path"));
|
|
23
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
24
|
+
const typedi_1 = require("typedi");
|
|
25
|
+
const IOSStreamService_1 = __importDefault(require("../../src/device-managers/ios/IOSStreamService"));
|
|
26
|
+
const AndroidStreamService_1 = __importDefault(require("../../src/device-managers/android/AndroidStreamService"));
|
|
27
|
+
const singleFlight_1 = require("../../src/helpers/singleFlight");
|
|
28
|
+
const PortAllocator_1 = require("../../src/services/PortAllocator");
|
|
29
|
+
const ProcessRegistry_1 = require("../../src/services/ProcessRegistry");
|
|
30
|
+
const device_store_1 = require("../../src/data-service/device-store");
|
|
31
|
+
const InternalHttpClient_1 = require("../../src/InternalHttpClient");
|
|
32
|
+
const app_service_1 = require("../../src/dashboard/services/app-service");
|
|
33
|
+
const scratch_port_leases_1 = require("../helpers/scratch-port-leases");
|
|
34
|
+
/**
|
|
35
|
+
* Seen on the lab (S9+ 381103b720057ece, iPhone 00008150-000E78612168C01C):
|
|
36
|
+
* the iPhone's live preview and recording showed the S9+'s screen, because
|
|
37
|
+
* both streams ended up on local port 9100.
|
|
38
|
+
*
|
|
39
|
+
* - A retried iOS start deleted its own port leases: acquire() handed the
|
|
40
|
+
* iPhone back its existing 8100/9100 leases, then the stale-session cleanup
|
|
41
|
+
* released "its" ports — the same numbers — so the new stream ran on
|
|
42
|
+
* unleased ports.
|
|
43
|
+
* - The allocator's OS probe bound 127.0.0.1, which macOS allows beside
|
|
44
|
+
* iproxy's IPv6 wildcard listener, so it called 9100 free and leased it to
|
|
45
|
+
* the S9+.
|
|
46
|
+
*
|
|
47
|
+
* Processes are fakes: an "iproxy" here is a real listener on `::` at its
|
|
48
|
+
* local port, which is what the real one is (`lsof`: `IPv6 TCP *:9100`).
|
|
49
|
+
* Nothing is spawned, and the lease table is a scratch SQLite file.
|
|
50
|
+
*/
|
|
51
|
+
const IPHONE = 'test-iphone-00008150';
|
|
52
|
+
const S9 = 'test-android-s9';
|
|
53
|
+
class FakeProcess extends events_1.EventEmitter {
|
|
54
|
+
constructor(command, args, server) {
|
|
55
|
+
super();
|
|
56
|
+
this.command = command;
|
|
57
|
+
this.args = args;
|
|
58
|
+
this.server = server;
|
|
59
|
+
this.pid = undefined;
|
|
60
|
+
this.exitCode = null;
|
|
61
|
+
this.stdout = new events_1.EventEmitter();
|
|
62
|
+
this.stderr = new events_1.EventEmitter();
|
|
63
|
+
}
|
|
64
|
+
kill() {
|
|
65
|
+
var _a;
|
|
66
|
+
if (this.exitCode === null)
|
|
67
|
+
this.exitCode = 137;
|
|
68
|
+
if ((_a = this.server) === null || _a === void 0 ? void 0 : _a.listening)
|
|
69
|
+
this.server.close();
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Bind synchronously, as a real iproxy has by the time its process runs. */
|
|
74
|
+
function listenNow(port, host = '::') {
|
|
75
|
+
const server = net_1.default.createServer((socket) => {
|
|
76
|
+
server.emit('accepted');
|
|
77
|
+
socket.destroy();
|
|
78
|
+
});
|
|
79
|
+
server.on('error', () => undefined);
|
|
80
|
+
server.listen(port, host);
|
|
81
|
+
return server;
|
|
82
|
+
}
|
|
83
|
+
function iosService() {
|
|
84
|
+
// Bypass the constructor: it starts watchdog intervals.
|
|
85
|
+
const svc = Object.create(IOSStreamService_1.default.prototype);
|
|
86
|
+
svc.sessions = new Map();
|
|
87
|
+
svc.startFlight = new singleFlight_1.SingleFlight();
|
|
88
|
+
svc.recoveryCooldowns = new Map();
|
|
89
|
+
svc.RECOVERY_COOLDOWN_MS = 30000;
|
|
90
|
+
svc.STREAM_PORT_TTL_MS = 90 * 60 * 1000;
|
|
91
|
+
svc.goIOSPath = '/nonexistent/go-ios';
|
|
92
|
+
return svc;
|
|
93
|
+
}
|
|
94
|
+
function androidService() {
|
|
95
|
+
const svc = Object.create(AndroidStreamService_1.default.prototype);
|
|
96
|
+
svc.sessions = new Map();
|
|
97
|
+
svc.startFlight = new singleFlight_1.SingleFlight();
|
|
98
|
+
return svc;
|
|
99
|
+
}
|
|
100
|
+
describe('iOS stream port leases (fake processes, real sockets, scratch DB)', () => {
|
|
101
|
+
const scratch = (0, scratch_port_leases_1.useScratchPortLeases)();
|
|
102
|
+
let allocator;
|
|
103
|
+
let devices;
|
|
104
|
+
let spawned;
|
|
105
|
+
let extraServers;
|
|
106
|
+
let appiumWdaPorts;
|
|
107
|
+
let ios;
|
|
108
|
+
let android;
|
|
109
|
+
beforeEach(() => {
|
|
110
|
+
allocator = new PortAllocator_1.PortAllocator();
|
|
111
|
+
// iOS and Android share the mjpeg range, as they do on the lab.
|
|
112
|
+
allocator.configure({
|
|
113
|
+
mjpeg: [scratch.base, scratch.base + 9],
|
|
114
|
+
wda: [scratch.base + 50, scratch.base + 59],
|
|
115
|
+
});
|
|
116
|
+
devices = new Map([
|
|
117
|
+
[IPHONE, { udid: IPHONE, host: 'h', platform: 'ios', busy: false, session_id: null }],
|
|
118
|
+
[S9, { udid: S9, host: 'h', platform: 'android', busy: false, session_id: null }],
|
|
119
|
+
]);
|
|
120
|
+
spawned = [];
|
|
121
|
+
extraServers = [];
|
|
122
|
+
appiumWdaPorts = new Set();
|
|
123
|
+
sinon_1.default.stub(process, 'kill');
|
|
124
|
+
const real = typedi_1.Container.get.bind(typedi_1.Container);
|
|
125
|
+
sinon_1.default.stub(typedi_1.Container, 'get').callsFake((token) => {
|
|
126
|
+
if (token === PortAllocator_1.PortAllocator)
|
|
127
|
+
return allocator;
|
|
128
|
+
if (token === ProcessRegistry_1.ProcessRegistry)
|
|
129
|
+
return { track: () => undefined };
|
|
130
|
+
return real(token);
|
|
131
|
+
});
|
|
132
|
+
sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({
|
|
133
|
+
findDevice: (_a) => __awaiter(void 0, [_a], void 0, function* ({ udid }) { return devices.get(udid); }),
|
|
134
|
+
updateDevice: (udid, _host, data) => __awaiter(void 0, void 0, void 0, function* () { return Object.assign(devices.get(udid), data); }),
|
|
135
|
+
});
|
|
136
|
+
sinon_1.default.stub(child_process_1.default, 'spawn').callsFake(((command, args) => {
|
|
137
|
+
let server;
|
|
138
|
+
if (command === 'iproxy') {
|
|
139
|
+
server = listenNow(Number(args[args.length - 1].split(':')[0]));
|
|
140
|
+
}
|
|
141
|
+
const proc = new FakeProcess(command, args, server);
|
|
142
|
+
spawned.push(proc);
|
|
143
|
+
return proc;
|
|
144
|
+
}));
|
|
145
|
+
sinon_1.default.stub(app_service_1.APP_SERVICE, 'getWDAApp').resolves(null);
|
|
146
|
+
sinon_1.default.stub(InternalHttpClient_1.InternalHttpClient, 'get').rejects(new Error('no WDA in tests'));
|
|
147
|
+
ios = iosService();
|
|
148
|
+
// WDA answers on a port when an Appium session forwards it, or when this
|
|
149
|
+
// service's own iproxy forwards it while its runwda is up.
|
|
150
|
+
sinon_1.default.stub(ios, 'isWDARunning').callsFake((...args) => __awaiter(void 0, void 0, void 0, function* () {
|
|
151
|
+
const port = args[0];
|
|
152
|
+
if (appiumWdaPorts.has(port))
|
|
153
|
+
return true;
|
|
154
|
+
const runwda = spawned.some((p) => p.command === ios.goIOSPath && p.exitCode === null);
|
|
155
|
+
return (runwda &&
|
|
156
|
+
spawned.some((p) => p.command === 'iproxy' && p.exitCode === null && p.args.includes(`${port}:8100`)));
|
|
157
|
+
}));
|
|
158
|
+
sinon_1.default.stub(ios, 'cleanupOrphanTunnels').resolves();
|
|
159
|
+
sinon_1.default.stub(ios, 'killStaleProcesses').resolves();
|
|
160
|
+
sinon_1.default.stub(ios, 'ensureTunnel').resolves(null);
|
|
161
|
+
sinon_1.default.stub(ios, 'detectWDABundleId').resolves('com.test.WebDriverAgentRunner.xctrunner');
|
|
162
|
+
sinon_1.default.stub(ios, 'updateWDASettings').resolves();
|
|
163
|
+
sinon_1.default.stub(ios, 'createWDASession').resolves(null);
|
|
164
|
+
android = androidService();
|
|
165
|
+
sinon_1.default.stub(android, 'resolveAdbInvocation').resolves({ adbPath: 'adb', adbHostArgs: [] });
|
|
166
|
+
sinon_1.default.stub(android, 'captureLoop').callsFake((...args) => {
|
|
167
|
+
args[1].latestFrame = Buffer.from([0xff, 0xd8]);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
171
|
+
var _a;
|
|
172
|
+
for (const p of spawned)
|
|
173
|
+
p.kill();
|
|
174
|
+
for (const s of extraServers)
|
|
175
|
+
if (s.listening)
|
|
176
|
+
s.close();
|
|
177
|
+
for (const s of android.sessions.values())
|
|
178
|
+
(_a = s.server) === null || _a === void 0 ? void 0 : _a.close();
|
|
179
|
+
sinon_1.default.restore();
|
|
180
|
+
}));
|
|
181
|
+
after(() => {
|
|
182
|
+
const runLog = path_1.default.join(os_1.default.tmpdir(), 'xenon-logs', `runwda-${IPHONE}.log`);
|
|
183
|
+
if (fs_1.default.existsSync(runLog))
|
|
184
|
+
fs_1.default.unlinkSync(runLog);
|
|
185
|
+
});
|
|
186
|
+
function leaseOf(port) {
|
|
187
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
188
|
+
return scratch.db.portLease.findUnique({ where: { port } });
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
/** A start that fails after taking its ports: go-ios missing, say. */
|
|
192
|
+
function failedIosStart() {
|
|
193
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
194
|
+
var _a;
|
|
195
|
+
const goIos = sinon_1.default.stub(ios, 'isGoIOSAvailable').resolves(false);
|
|
196
|
+
let err;
|
|
197
|
+
yield ios.startStream(IPHONE).catch((e) => (err = e));
|
|
198
|
+
(0, chai_1.expect)(err === null || err === void 0 ? void 0 : err.message).to.equal('go-ios not available');
|
|
199
|
+
(0, chai_1.expect)((_a = ios.getStreamStatus(IPHONE)) === null || _a === void 0 ? void 0 : _a.status, 'the failed session stays in the map').to.equal('error');
|
|
200
|
+
goIos.restore();
|
|
201
|
+
// The retry comes after the start cooldown has run out.
|
|
202
|
+
ios.recoveryCooldowns.clear();
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
it('a start after a failed start holds leases on both of its ports', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
206
|
+
var _a;
|
|
207
|
+
yield failedIosStart();
|
|
208
|
+
sinon_1.default.stub(ios, 'isGoIOSAvailable').resolves(true);
|
|
209
|
+
const { wdaPort, mjpegPort } = yield ios.startStream(IPHONE);
|
|
210
|
+
(0, chai_1.expect)((_a = ios.getStreamStatus(IPHONE)) === null || _a === void 0 ? void 0 : _a.status).to.equal('running');
|
|
211
|
+
const wda = yield leaseOf(wdaPort);
|
|
212
|
+
const mjpeg = yield leaseOf(mjpegPort);
|
|
213
|
+
(0, chai_1.expect)(wda === null || wda === void 0 ? void 0 : wda.leasedToUdid, `wda port ${wdaPort} must stay leased to the iPhone`).to.equal(IPHONE);
|
|
214
|
+
(0, chai_1.expect)(wda === null || wda === void 0 ? void 0 : wda.purpose).to.equal('wda');
|
|
215
|
+
(0, chai_1.expect)(mjpeg === null || mjpeg === void 0 ? void 0 : mjpeg.leasedToUdid, `mjpeg port ${mjpegPort} must stay leased to the iPhone`).to.equal(IPHONE);
|
|
216
|
+
(0, chai_1.expect)(mjpeg === null || mjpeg === void 0 ? void 0 : mjpeg.purpose).to.equal('mjpeg');
|
|
217
|
+
}));
|
|
218
|
+
it('stopping a stale iOS session never deletes a lease another device holds', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
219
|
+
var _a;
|
|
220
|
+
// The S9+ has since been leased the port the stale iPhone session names.
|
|
221
|
+
const now = Date.now();
|
|
222
|
+
yield scratch.db.portLease.create({
|
|
223
|
+
data: {
|
|
224
|
+
port: scratch.base,
|
|
225
|
+
purpose: 'mjpeg',
|
|
226
|
+
leasedToUdid: S9,
|
|
227
|
+
leasedAt: now,
|
|
228
|
+
expiresAt: now + 60000,
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
ios.sessions.set(IPHONE, {
|
|
232
|
+
udid: IPHONE,
|
|
233
|
+
wdaProcess: null,
|
|
234
|
+
forwardWDAProcess: null,
|
|
235
|
+
forwardMJPEGProcess: null,
|
|
236
|
+
tunnelProcess: null,
|
|
237
|
+
wdaPort: scratch.base + 50,
|
|
238
|
+
mjpegPort: scratch.base,
|
|
239
|
+
status: 'error',
|
|
240
|
+
lastViewerAt: Date.now(),
|
|
241
|
+
viewerCount: 0,
|
|
242
|
+
});
|
|
243
|
+
yield ios.stopStream(IPHONE);
|
|
244
|
+
(0, chai_1.expect)((_a = (yield leaseOf(scratch.base))) === null || _a === void 0 ? void 0 : _a.leasedToUdid, "the S9+'s lease must survive").to.equal(S9);
|
|
245
|
+
}));
|
|
246
|
+
it("an Android stream never gets the port a retried iOS stream is using (the lab's 9100)", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
247
|
+
var _a;
|
|
248
|
+
yield failedIosStart();
|
|
249
|
+
sinon_1.default.stub(ios, 'isGoIOSAvailable').resolves(true);
|
|
250
|
+
const { mjpegPort: iphonePort } = yield ios.startStream(IPHONE);
|
|
251
|
+
const iproxy = (_a = spawned.find((p) => p.command === 'iproxy' && p.args.includes(`${iphonePort}:9100`))) === null || _a === void 0 ? void 0 : _a.server;
|
|
252
|
+
if (!iproxy)
|
|
253
|
+
throw new Error('no iproxy was spawned for the iPhone mjpeg port');
|
|
254
|
+
(0, chai_1.expect)(iproxy.listening, "the iPhone's iproxy is listening").to.equal(true);
|
|
255
|
+
const { mjpegPort: s9Port } = yield android.startStream(S9);
|
|
256
|
+
(0, chai_1.expect)(s9Port, 'the S9+ must be given a different port than the iPhone').to.not.equal(iphonePort);
|
|
257
|
+
// And the symptom itself: 127.0.0.1:<iPhone port> still reaches the iPhone.
|
|
258
|
+
const reached = new Promise((resolve) => {
|
|
259
|
+
iproxy.once('accepted', () => resolve(true));
|
|
260
|
+
setTimeout(() => resolve(false), 1000);
|
|
261
|
+
});
|
|
262
|
+
net_1.default.connect(iphonePort, '127.0.0.1').on('error', () => undefined);
|
|
263
|
+
(0, chai_1.expect)(yield reached, "127.0.0.1:<iPhone port> must reach the iPhone's iproxy").to.equal(true);
|
|
264
|
+
}));
|
|
265
|
+
describe('attaching to the WDA an Appium session runs', () => {
|
|
266
|
+
it("attaches on the device's WDA port and leases it, without launching a second WDA", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
267
|
+
var _a, _b;
|
|
268
|
+
const appiumPort = scratch.base + 50;
|
|
269
|
+
Object.assign(devices.get(IPHONE), {
|
|
270
|
+
busy: true,
|
|
271
|
+
session_id: '7f0c5a52-appium-session',
|
|
272
|
+
wdaLocalPort: appiumPort,
|
|
273
|
+
});
|
|
274
|
+
// The xcuitest driver's forwarder: net.Server.listen(port), i.e. `::`.
|
|
275
|
+
extraServers.push(listenNow(appiumPort));
|
|
276
|
+
appiumWdaPorts.add(appiumPort);
|
|
277
|
+
const goIos = sinon_1.default.stub(ios, 'isGoIOSAvailable').resolves(true);
|
|
278
|
+
const { wdaPort, mjpegPort } = yield ios.startStream(IPHONE);
|
|
279
|
+
(0, chai_1.expect)(wdaPort).to.equal(appiumPort);
|
|
280
|
+
(0, chai_1.expect)(goIos.called, 'must not start a WDA of its own').to.equal(false);
|
|
281
|
+
(0, chai_1.expect)(spawned.some((p) => p.command === ios.goIOSPath)).to.equal(false);
|
|
282
|
+
(0, chai_1.expect)((_a = (yield leaseOf(appiumPort))) === null || _a === void 0 ? void 0 : _a.leasedToUdid).to.equal(IPHONE);
|
|
283
|
+
(0, chai_1.expect)((_b = (yield leaseOf(mjpegPort))) === null || _b === void 0 ? void 0 : _b.leasedToUdid).to.equal(IPHONE);
|
|
284
|
+
}));
|
|
285
|
+
it('does not attach to a WDA on that port when no Appium session holds the device', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
286
|
+
// WDA's /status carries no udid, so an answer on a port the Device row
|
|
287
|
+
// names proves nothing on its own: two rows can carry the same stale
|
|
288
|
+
// port, and the answer may be another phone's.
|
|
289
|
+
const strayPort = scratch.base + 50;
|
|
290
|
+
Object.assign(devices.get(IPHONE), { wdaLocalPort: strayPort });
|
|
291
|
+
extraServers.push(listenNow(strayPort));
|
|
292
|
+
appiumWdaPorts.add(strayPort);
|
|
293
|
+
sinon_1.default.stub(ios, 'isGoIOSAvailable').resolves(true);
|
|
294
|
+
const { wdaPort } = yield ios.startStream(IPHONE);
|
|
295
|
+
(0, chai_1.expect)(wdaPort).to.not.equal(strayPort);
|
|
296
|
+
(0, chai_1.expect)(spawned.some((p) => p.command === ios.goIOSPath), 'starts its own WDA').to.equal(true);
|
|
297
|
+
}));
|
|
298
|
+
});
|
|
299
|
+
});
|
|
@@ -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
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const http_1 = __importDefault(require("http"));
|
|
18
|
+
const IOSStreamService_1 = __importDefault(require("../../src/device-managers/ios/IOSStreamService"));
|
|
19
|
+
/**
|
|
20
|
+
* isWDARunning took a udid and compared it with `value.ios.udid` from WDA's
|
|
21
|
+
* /status, which WDA never sends. The check could never refuse anything.
|
|
22
|
+
*
|
|
23
|
+
* What WDA does send (WebDriverAgent, FBSessionCommands.m handleGetStatus):
|
|
24
|
+
* ready, message, state, os {name, version, sdkVersion,
|
|
25
|
+
* testmanagerdVersion}, ios {ip} (plus simulatorVersion on a simulator),
|
|
26
|
+
* build and device (the form factor). /wda/device/info
|
|
27
|
+
* (FBCustomCommands.m) has a `uuid`, but that is identifierForVendor, not
|
|
28
|
+
* the phone's UDID. No endpoint names the phone.
|
|
29
|
+
*
|
|
30
|
+
* The fake WDA below listens on an OS-assigned port on 127.0.0.1.
|
|
31
|
+
*/
|
|
32
|
+
const REAL_WDA_STATUS = {
|
|
33
|
+
value: {
|
|
34
|
+
ready: true,
|
|
35
|
+
message: 'WebDriverAgent is ready to accept commands',
|
|
36
|
+
state: 'success',
|
|
37
|
+
os: { name: 'iOS', version: '26.7', sdkVersion: '26.0', testmanagerdVersion: 65535 },
|
|
38
|
+
ios: { ip: '192.168.0.23' },
|
|
39
|
+
build: {
|
|
40
|
+
time: 'Jul 14 2026 08:53:17',
|
|
41
|
+
productBundleIdentifier: 'com.facebook.WebDriverAgentRunner',
|
|
42
|
+
version: '11.4.1',
|
|
43
|
+
},
|
|
44
|
+
device: 'iphone',
|
|
45
|
+
},
|
|
46
|
+
sessionId: null,
|
|
47
|
+
};
|
|
48
|
+
describe('isWDARunning: WDA names no device', () => {
|
|
49
|
+
let server;
|
|
50
|
+
let port;
|
|
51
|
+
let requests;
|
|
52
|
+
let answers;
|
|
53
|
+
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
54
|
+
requests = 0;
|
|
55
|
+
answers = [];
|
|
56
|
+
server = http_1.default.createServer((req, res) => {
|
|
57
|
+
var _a;
|
|
58
|
+
requests++;
|
|
59
|
+
const status = (_a = answers.shift()) !== null && _a !== void 0 ? _a : 200;
|
|
60
|
+
res.writeHead(status, { 'content-type': 'application/json' });
|
|
61
|
+
res.end(JSON.stringify(status === 200 ? REAL_WDA_STATUS : { value: { error: 'busy' } }));
|
|
62
|
+
});
|
|
63
|
+
yield new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
64
|
+
port = server.address().port;
|
|
65
|
+
}));
|
|
66
|
+
afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
67
|
+
yield new Promise((resolve) => server.close(() => resolve()));
|
|
68
|
+
}));
|
|
69
|
+
function service() {
|
|
70
|
+
// Bypass the constructor: it starts watchdog intervals.
|
|
71
|
+
return Object.create(IOSStreamService_1.default.prototype);
|
|
72
|
+
}
|
|
73
|
+
it("answers ready from WDA's real /status, which carries no udid", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
74
|
+
(0, chai_1.expect)(yield service().isWDARunning(port)).to.equal(true);
|
|
75
|
+
(0, chai_1.expect)(JSON.stringify(REAL_WDA_STATUS)).to.not.match(/udid/i);
|
|
76
|
+
}));
|
|
77
|
+
it('takes the retry budget as its second argument: 0 means one attempt', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
78
|
+
// A transient 5xx is retried; with no retries asked for it is the answer.
|
|
79
|
+
answers = [503, 200];
|
|
80
|
+
const up = yield service().isWDARunning(port, 0);
|
|
81
|
+
(0, chai_1.expect)(requests, 'one attempt').to.equal(1);
|
|
82
|
+
(0, chai_1.expect)(up).to.equal(false);
|
|
83
|
+
}));
|
|
84
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
const chai_1 = require("chai");
|
|
16
|
+
const http_1 = __importDefault(require("http"));
|
|
17
|
+
const helpers_1 = require("../../src/helpers");
|
|
18
|
+
/**
|
|
19
|
+
* A hub asks whether each node is alive, and a node whether its hub is, with
|
|
20
|
+
* isXenonRunning. With auth on, every /xenon/api route but /health needs a
|
|
21
|
+
* login, and neither side logs in for this probe: a probe of a login-only
|
|
22
|
+
* route called a running server dead, so the hub pruned its node's phones and
|
|
23
|
+
* the node never registered with its hub.
|
|
24
|
+
*/
|
|
25
|
+
describe('isXenonRunning', () => {
|
|
26
|
+
let server;
|
|
27
|
+
let base;
|
|
28
|
+
const hits = [];
|
|
29
|
+
/** Answers the way a Xenon server with auth on does. */
|
|
30
|
+
function authEnabledXenon(req, res) {
|
|
31
|
+
hits.push(String(req.url));
|
|
32
|
+
if (req.url === '/xenon/api/health') {
|
|
33
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
34
|
+
res.end(JSON.stringify({ ok: true }));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
res.writeHead(401, { 'content-type': 'application/json' });
|
|
38
|
+
res.end(JSON.stringify({ error: 'unauthenticated' }));
|
|
39
|
+
}
|
|
40
|
+
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
41
|
+
hits.length = 0;
|
|
42
|
+
server = http_1.default.createServer(authEnabledXenon);
|
|
43
|
+
yield new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
44
|
+
base = `http://127.0.0.1:${server.address().port}`;
|
|
45
|
+
}));
|
|
46
|
+
afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
47
|
+
yield new Promise((resolve) => server.close(() => resolve()));
|
|
48
|
+
}));
|
|
49
|
+
it('calls a server with auth on running', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
50
|
+
(0, chai_1.expect)(yield (0, helpers_1.isXenonRunning)(base)).to.equal(true);
|
|
51
|
+
(0, chai_1.expect)(hits).to.deep.equal(['/xenon/api/health']);
|
|
52
|
+
}));
|
|
53
|
+
it('calls a server that is not listening not running', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
54
|
+
// A port that was free a moment ago: bound, read, then closed again.
|
|
55
|
+
const gone = http_1.default.createServer();
|
|
56
|
+
yield new Promise((resolve) => gone.listen(0, '127.0.0.1', resolve));
|
|
57
|
+
const port = gone.address().port;
|
|
58
|
+
yield new Promise((resolve) => gone.close(() => resolve()));
|
|
59
|
+
(0, chai_1.expect)(yield (0, helpers_1.isXenonRunning)(`http://127.0.0.1:${port}`)).to.equal(false);
|
|
60
|
+
}));
|
|
61
|
+
it('calls a server whose health route fails not running', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
62
|
+
server.removeAllListeners('request');
|
|
63
|
+
server.on('request', (_req, res) => {
|
|
64
|
+
res.writeHead(503);
|
|
65
|
+
res.end();
|
|
66
|
+
});
|
|
67
|
+
(0, chai_1.expect)(yield (0, helpers_1.isXenonRunning)(base)).to.equal(false);
|
|
68
|
+
}));
|
|
69
|
+
});
|
|
@@ -62,7 +62,7 @@ describe('LeaseOrphanSweeper', () => {
|
|
|
62
62
|
},
|
|
63
63
|
portLease: { deleteMany: sinon_1.default.stub().resolves({ count: 0 }) },
|
|
64
64
|
};
|
|
65
|
-
storeStub = {
|
|
65
|
+
storeStub = { releaseLeaseLock: sinon_1.default.stub().resolves(true) };
|
|
66
66
|
const { LeaseOrphanSweeper } = yield Promise.resolve().then(() => __importStar(require('../../../src/services/lease/LeaseOrphanSweeper')));
|
|
67
67
|
sweeper = new LeaseOrphanSweeper(prismaStub, storeStub);
|
|
68
68
|
}));
|
|
@@ -76,7 +76,7 @@ describe('LeaseOrphanSweeper', () => {
|
|
|
76
76
|
(0, chai_1.expect)(prismaStub.lease.update.firstCall.args[0].where.id).to.equal('lse_1');
|
|
77
77
|
(0, chai_1.expect)(prismaStub.lease.update.firstCall.args[0].data.status).to.equal('expired');
|
|
78
78
|
(0, chai_1.expect)(prismaStub.portLease.deleteMany.calledWith({ where: { leaseId: 'lse_1' } })).to.equal(true);
|
|
79
|
-
(0, chai_1.expect)(storeStub.
|
|
79
|
+
(0, chai_1.expect)(storeStub.releaseLeaseLock.calledOnceWith('u1', 'h1')).to.equal(true);
|
|
80
80
|
}));
|
|
81
81
|
it('no-op when no stale leases', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
82
82
|
prismaStub.lease.findMany.resolves([]);
|
|
@@ -72,6 +72,7 @@ describe('LeaseService', () => {
|
|
|
72
72
|
udid: 'u1', host: 'h1', platform: 'android', sdk: '14', name: 'Pixel 7', teamId: null,
|
|
73
73
|
}),
|
|
74
74
|
updateDevice: sinon_1.default.stub().resolves(),
|
|
75
|
+
releaseLeaseLock: sinon_1.default.stub().resolves(true),
|
|
75
76
|
getDevices: sinon_1.default.stub().resolves([]),
|
|
76
77
|
};
|
|
77
78
|
portClientStub = {
|
|
@@ -222,6 +223,30 @@ describe('LeaseService', () => {
|
|
|
222
223
|
(0, chai_1.expect)(out.expiresAt).to.be.at.least(Date.now() + 50000);
|
|
223
224
|
(0, chai_1.expect)(prismaStub.lease.updateMany.firstCall.args[0].where.status).to.equal('active');
|
|
224
225
|
}));
|
|
226
|
+
// The sweeper marks a lease by missed heartbeats, not by expiresAt, so a
|
|
227
|
+
// lease can be past expiresAt and still 'active'. Heartbeat already
|
|
228
|
+
// refuses it; extend must too, or it hands the holder the lease back.
|
|
229
|
+
for (const op of ['heartbeat', 'extend']) {
|
|
230
|
+
it(`${op} refuses a lease past its expiresAt that the sweeper hasn't marked, and writes nothing`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
231
|
+
const { hashToken } = yield Promise.resolve().then(() => __importStar(require('../../../src/services/lease/leaseToken')));
|
|
232
|
+
const { LeaseGone } = yield Promise.resolve().then(() => __importStar(require('../../../src/services/lease/LeaseService')));
|
|
233
|
+
const tok = 'd'.repeat(64);
|
|
234
|
+
const expiresAt = Date.now() - 1000;
|
|
235
|
+
prismaStub.lease.findUnique.resolves({
|
|
236
|
+
id: 'lse_test',
|
|
237
|
+
tokenHash: hashToken(tok),
|
|
238
|
+
status: 'active',
|
|
239
|
+
expiresAt,
|
|
240
|
+
createdAt: new Date(Date.now() - 60000),
|
|
241
|
+
});
|
|
242
|
+
const call = op === 'extend' ? svc.extend('lse_test', tok, 60000) : svc.heartbeat('lse_test', tok);
|
|
243
|
+
let thrown = null;
|
|
244
|
+
yield call.catch((e) => (thrown = e));
|
|
245
|
+
(0, chai_1.expect)(thrown).to.be.instanceOf(LeaseGone);
|
|
246
|
+
(0, chai_1.expect)(thrown.message).to.equal(`lease lse_test expired at ${expiresAt}`);
|
|
247
|
+
(0, chai_1.expect)(prismaStub.lease.updateMany.called).to.equal(false);
|
|
248
|
+
}));
|
|
249
|
+
}
|
|
225
250
|
it('release sets status=released and cascades PortLease delete', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
226
251
|
const { hashToken } = yield Promise.resolve().then(() => __importStar(require('../../../src/services/lease/leaseToken')));
|
|
227
252
|
const tok = 'c'.repeat(64);
|
|
@@ -233,7 +258,9 @@ describe('LeaseService', () => {
|
|
|
233
258
|
(0, chai_1.expect)(prismaStub.lease.updateMany.firstCall.args[0].data.status).to.equal('released');
|
|
234
259
|
(0, chai_1.expect)(prismaStub.lease.updateMany.firstCall.args[0].where.status).to.equal('active');
|
|
235
260
|
(0, chai_1.expect)(prismaStub.portLease.deleteMany.firstCall.args[0].where.leaseId).to.equal('lse_test');
|
|
236
|
-
|
|
261
|
+
// Only the lease's lock (a session on the phone keeps it busy).
|
|
262
|
+
(0, chai_1.expect)(storeStub.releaseLeaseLock.calledWith('u1', 'h1')).to.equal(true);
|
|
263
|
+
(0, chai_1.expect)(storeStub.updateDevice.called).to.equal(false);
|
|
237
264
|
}));
|
|
238
265
|
it('rejects operations with mismatched token', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
239
266
|
const { hashToken } = yield Promise.resolve().then(() => __importStar(require('../../../src/services/lease/leaseToken')));
|