@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
|
@@ -17,6 +17,7 @@ exports.parseH264WsPath = parseH264WsPath;
|
|
|
17
17
|
exports.attachH264Ws = attachH264Ws;
|
|
18
18
|
const ws_1 = require("ws");
|
|
19
19
|
const logger_1 = __importDefault(require("../../logger"));
|
|
20
|
+
const upgradeRouter_1 = require("./upgradeRouter");
|
|
20
21
|
/**
|
|
21
22
|
* Authenticated H.264 video WebSocket.
|
|
22
23
|
*
|
|
@@ -44,18 +45,22 @@ function parseH264WsPath(url) {
|
|
|
44
45
|
}
|
|
45
46
|
/**
|
|
46
47
|
* Attach the h264 WebSocket upgrade handler to the plugin's HTTP server. Only
|
|
47
|
-
* claims `/xenon/api/control/:udid/stream/h264`
|
|
48
|
-
*
|
|
49
|
-
*
|
|
48
|
+
* claims `/xenon/api/control/:udid/stream/h264` (with a ticket), as a route of
|
|
49
|
+
* the server's upgrade router, so no other handler sees these sockets and this
|
|
50
|
+
* one sees no others (upgradeRouter.ts). Every connection must redeem a valid
|
|
51
|
+
* stream ticket before any video flows.
|
|
50
52
|
*/
|
|
51
53
|
function attachH264Ws(server, deps) {
|
|
52
54
|
var _a;
|
|
53
55
|
const wss = new ws_1.WebSocketServer({ noServer: true });
|
|
54
56
|
const maxBuffered = (_a = deps.maxBufferedBytes) !== null && _a !== void 0 ? _a : 4 * 1024 * 1024;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
(0, upgradeRouter_1.upgradeRouterFor)(server).add({
|
|
58
|
+
name: 'H.264 live preview',
|
|
59
|
+
matches: (req) => !!req.url && parseH264WsPath(req.url) !== null,
|
|
60
|
+
handle: upgrade,
|
|
61
|
+
});
|
|
62
|
+
function upgrade(req, socket, head) {
|
|
63
|
+
const parsed = parseH264WsPath(req.url);
|
|
59
64
|
wss.handleUpgrade(req, socket, head, (ws) => __awaiter(this, void 0, void 0, function* () {
|
|
60
65
|
// Register cleanup BEFORE the awaits below. redeem + startStream can take
|
|
61
66
|
// seconds; if the client disconnects during that window we must still
|
|
@@ -111,5 +116,5 @@ function attachH264Ws(server, deps) {
|
|
|
111
116
|
});
|
|
112
117
|
logger_1.default.info(`[${parsed.udid}] H.264 WS client connected`);
|
|
113
118
|
}));
|
|
114
|
-
}
|
|
119
|
+
}
|
|
115
120
|
}
|
|
@@ -16,6 +16,7 @@ exports.parseLogcatWsPath = parseLogcatWsPath;
|
|
|
16
16
|
exports.attachLogcatWs = attachLogcatWs;
|
|
17
17
|
const ws_1 = require("ws");
|
|
18
18
|
const logger_1 = __importDefault(require("../../logger"));
|
|
19
|
+
const upgradeRouter_1 = require("./upgradeRouter");
|
|
19
20
|
const ostraceParse_1 = require("../../services/logcat/ostraceParse");
|
|
20
21
|
const LOGCAT_PATH_RE = /^\/xenon\/api\/control\/([^/]+)\/logcat$/;
|
|
21
22
|
/** Parse the logcat stream upgrade path; returns null for any non-matching URL. */
|
|
@@ -42,8 +43,9 @@ function parseLogcatWsPath(url) {
|
|
|
42
43
|
}
|
|
43
44
|
/**
|
|
44
45
|
* Attach the logcat WebSocket upgrade handler to the plugin's HTTP server.
|
|
45
|
-
* Only claims `/xenon/api/control/:udid/logcat`
|
|
46
|
-
* the
|
|
46
|
+
* Only claims `/xenon/api/control/:udid/logcat` (with a ticket), as a route of
|
|
47
|
+
* the server's upgrade router, so no other handler sees these sockets and this
|
|
48
|
+
* one sees no others (upgradeRouter.ts).
|
|
47
49
|
*
|
|
48
50
|
* Authentication mirrors `attachH264Ws`: a single-use, udid-bound stream
|
|
49
51
|
* ticket. Authorization does not — device logs routinely carry auth tokens,
|
|
@@ -58,10 +60,13 @@ function attachLogcatWs(server, deps) {
|
|
|
58
60
|
var _a;
|
|
59
61
|
const wss = new ws_1.WebSocketServer({ noServer: true });
|
|
60
62
|
const maxBuffered = (_a = deps.maxBufferedBytes) !== null && _a !== void 0 ? _a : 4 * 1024 * 1024;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
(0, upgradeRouter_1.upgradeRouterFor)(server).add({
|
|
64
|
+
name: 'logcat',
|
|
65
|
+
matches: (req) => !!req.url && parseLogcatWsPath(req.url) !== null,
|
|
66
|
+
handle: upgrade,
|
|
67
|
+
});
|
|
68
|
+
function upgrade(req, socket, head) {
|
|
69
|
+
const parsed = parseLogcatWsPath(req.url);
|
|
65
70
|
wss.handleUpgrade(req, socket, head, (ws) => __awaiter(this, void 0, void 0, function* () {
|
|
66
71
|
var _a, _b;
|
|
67
72
|
// Register cleanup BEFORE the awaits below. redeem + authorize +
|
|
@@ -282,5 +287,5 @@ function attachLogcatWs(server, deps) {
|
|
|
282
287
|
};
|
|
283
288
|
logger_1.default.info(`[${parsed.udid}] logcat WS client connected`);
|
|
284
289
|
}));
|
|
285
|
-
}
|
|
290
|
+
}
|
|
286
291
|
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.UpgradeRouter = void 0;
|
|
7
|
+
exports.appiumPatternMatcher = appiumPatternMatcher;
|
|
8
|
+
exports.dispatchToAppiumHandlers = dispatchToAppiumHandlers;
|
|
9
|
+
exports.appiumUpgradeListener = appiumUpgradeListener;
|
|
10
|
+
exports.upgradeRouterFor = upgradeRouterFor;
|
|
11
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
12
|
+
const appiumBasePath_1 = require("../appiumBasePath");
|
|
13
|
+
function escapeRegExp(text) {
|
|
14
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The path-to-regexp v8 syntax Appium's patterns use: literal segments and
|
|
18
|
+
* `:name` parameters. Anything else (groups, wildcards, escapes) is not
|
|
19
|
+
* compiled, and such a pattern matches nothing here; see appiumPatternMatcher.
|
|
20
|
+
*/
|
|
21
|
+
const UNSUPPORTED_PATTERN_SYNTAX = /[{}()[\]*+?!\\"]/;
|
|
22
|
+
/** Splits a pattern into literal text (even indexes) and `:name` parameters (odd). */
|
|
23
|
+
const PARAM = /(:[A-Za-z_$][\w$]*)/;
|
|
24
|
+
const compiled = new Map();
|
|
25
|
+
const warnedPatterns = new Set();
|
|
26
|
+
/**
|
|
27
|
+
* base-driver matches an upgrade's pathname against each registered pattern
|
|
28
|
+
* with path-to-regexp's `match(pattern)` (v8, defaults): case-insensitive, an
|
|
29
|
+
* optional trailing slash, the whole pathname, each parameter one or more
|
|
30
|
+
* characters other than `/`, and each matched parameter decoded with
|
|
31
|
+
* decodeURIComponent, which throws on a malformed escape. This is that, for
|
|
32
|
+
* the patterns Appium and drivers register. It returns null for a pattern it
|
|
33
|
+
* does not speak.
|
|
34
|
+
*/
|
|
35
|
+
function appiumPatternMatcher(pattern) {
|
|
36
|
+
const cached = compiled.get(pattern);
|
|
37
|
+
if (cached !== undefined)
|
|
38
|
+
return cached;
|
|
39
|
+
let matcher = null;
|
|
40
|
+
if (!UNSUPPORTED_PATTERN_SYNTAX.test(pattern)) {
|
|
41
|
+
const source = pattern
|
|
42
|
+
.split(PARAM)
|
|
43
|
+
.map((part, i) => (i % 2 === 1 ? '([^/]+)' : escapeRegExp(part)))
|
|
44
|
+
.join('');
|
|
45
|
+
const regex = new RegExp(`^${source}(?:/)?$`, 'i');
|
|
46
|
+
matcher = (pathname) => {
|
|
47
|
+
const m = regex.exec(pathname);
|
|
48
|
+
if (!m)
|
|
49
|
+
return false;
|
|
50
|
+
// path-to-regexp decodes every matched parameter, and a malformed
|
|
51
|
+
// escape throws out of Appium's dispatch. Do the same, so this behaves
|
|
52
|
+
// as Appium's own listener does.
|
|
53
|
+
for (const value of m.slice(1))
|
|
54
|
+
decodeURIComponent(value);
|
|
55
|
+
return true;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
compiled.set(pattern, matcher);
|
|
59
|
+
return matcher;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* base-driver's `tryHandleWebSocketUpgrade`, over the server's public
|
|
63
|
+
* `webSocketsMapping`: hand a WebSocket upgrade to the first registered
|
|
64
|
+
* handler whose pattern matches its pathname. False when none does.
|
|
65
|
+
*/
|
|
66
|
+
function dispatchToAppiumHandlers(req, socket, head, mapping, logger) {
|
|
67
|
+
var _a, _b;
|
|
68
|
+
const upgrade = (_a = req.headers) === null || _a === void 0 ? void 0 : _a.upgrade;
|
|
69
|
+
if (typeof upgrade !== 'string' || upgrade.toLowerCase() !== 'websocket')
|
|
70
|
+
return false;
|
|
71
|
+
const pathname = (0, appiumBasePath_1.appiumPathname)((_b = req.url) !== null && _b !== void 0 ? _b : '');
|
|
72
|
+
for (const [pattern, wsServer] of Object.entries(mapping !== null && mapping !== void 0 ? mapping : {})) {
|
|
73
|
+
const matches = appiumPatternMatcher(pattern);
|
|
74
|
+
if (!matches) {
|
|
75
|
+
if (!warnedPatterns.has(pattern)) {
|
|
76
|
+
warnedPatterns.add(pattern);
|
|
77
|
+
logger.warn(`Appium WebSocket handler ${pattern} uses path syntax Xenon's copy of Appium's ` +
|
|
78
|
+
'upgrade listener does not match; it is skipped on this Node (< 22.21).');
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (matches(pathname)) {
|
|
83
|
+
wsServer.handleUpgrade(req, socket, head, (ws) => wsServer.emit('connection', ws, req));
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
logger.info(`Did not match the websocket upgrade request at ${pathname} to any known route`);
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The `upgrade` listener base-driver's configureHttp adds on Node >= 22.21:
|
|
92
|
+
* Appium's handlers take what they match, and every other socket is destroyed.
|
|
93
|
+
* `webSocketsMapping` is read per upgrade, since drivers add and remove
|
|
94
|
+
* handlers as sessions come and go.
|
|
95
|
+
*/
|
|
96
|
+
function appiumUpgradeListener(server, logger) {
|
|
97
|
+
return function appiumUpgradeThroughXenon(req, socket, head) {
|
|
98
|
+
if (!dispatchToAppiumHandlers(req, socket, head, server.webSocketsMapping, logger)) {
|
|
99
|
+
socket.destroy();
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** The upgrade's path without the query, which carries tickets. */
|
|
104
|
+
function pathOf(req) {
|
|
105
|
+
var _a;
|
|
106
|
+
return String((_a = req.url) !== null && _a !== void 0 ? _a : '').split('?')[0];
|
|
107
|
+
}
|
|
108
|
+
class UpgradeRouter {
|
|
109
|
+
constructor(server, logger = logger_1.default.scope('UpgradeRouter')) {
|
|
110
|
+
this.server = server;
|
|
111
|
+
this.logger = logger;
|
|
112
|
+
this.routes = [];
|
|
113
|
+
// base-driver's own test (`hasShouldUpgradeCallback`): without the
|
|
114
|
+
// callback it took upgrades in Express and added no listener. A server
|
|
115
|
+
// with no listener at all (not Appium's) needs one too, or neither Node
|
|
116
|
+
// emits `upgrade` and Xenon's routes are never reached.
|
|
117
|
+
const appiumListens = typeof server.shouldUpgradeCallback !== 'undefined' && server.listenerCount('upgrade') > 0;
|
|
118
|
+
if (appiumListens) {
|
|
119
|
+
this.appiumPath = 'appium-listener';
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
server.on('upgrade', appiumUpgradeListener(server, logger));
|
|
123
|
+
this.appiumPath = 'router-listener';
|
|
124
|
+
}
|
|
125
|
+
this.routeEmit();
|
|
126
|
+
}
|
|
127
|
+
/** Give every upgrade `route.matches` to `route.handle`, and to nothing else. */
|
|
128
|
+
add(route) {
|
|
129
|
+
this.routes.push(route);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Run `attach`, which attaches something that takes upgrades by adding an
|
|
133
|
+
* `upgrade` listener (socket.io). The listeners it added are taken off the
|
|
134
|
+
* server and become the route's handler, so they see only what `matches`.
|
|
135
|
+
*/
|
|
136
|
+
adopt(name, matches, attach) {
|
|
137
|
+
const before = new Set(this.server.listeners('upgrade'));
|
|
138
|
+
const attached = attach();
|
|
139
|
+
const added = this.server.listeners('upgrade').filter((l) => !before.has(l));
|
|
140
|
+
for (const listener of added)
|
|
141
|
+
this.server.removeListener('upgrade', listener);
|
|
142
|
+
if (added.length > 0) {
|
|
143
|
+
const server = this.server;
|
|
144
|
+
this.add({
|
|
145
|
+
name,
|
|
146
|
+
matches,
|
|
147
|
+
handle: (req, socket, head) => {
|
|
148
|
+
for (const listener of added)
|
|
149
|
+
listener.call(server, req, socket, head);
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return attached;
|
|
154
|
+
}
|
|
155
|
+
/** The route that owns an upgrade to `req`, or undefined when it is Appium's. */
|
|
156
|
+
routeFor(req) {
|
|
157
|
+
return this.routes.find((route) => route.matches(req));
|
|
158
|
+
}
|
|
159
|
+
/** Where the upgrades that are not Xenon's go, for the boot log. */
|
|
160
|
+
describeAppiumPath() {
|
|
161
|
+
return this.appiumPath === 'appium-listener'
|
|
162
|
+
? "Appium's own upgrade listener"
|
|
163
|
+
: "Appium's WebSocket handlers, through Xenon's copy of Appium's upgrade listener " +
|
|
164
|
+
'(this Node has no http.Server shouldUpgradeCallback, so Appium took upgrades in ' +
|
|
165
|
+
'Express, which no upgrade reaches once any upgrade listener exists)';
|
|
166
|
+
}
|
|
167
|
+
/** Close a socket whose upgrade could not be handed on, and say why. */
|
|
168
|
+
refuse(req, socket, what, error) {
|
|
169
|
+
var _a;
|
|
170
|
+
this.logger.error(`WebSocket upgrade ${pathOf(req)} ${what}: ${(_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : error}. Closing the socket.`);
|
|
171
|
+
socket.destroy();
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
routeEmit() {
|
|
175
|
+
const server = this.server;
|
|
176
|
+
const emit = server.emit;
|
|
177
|
+
const routeFor = (req) => this.routeFor(req);
|
|
178
|
+
const refuse = (req, socket, what, error) => this.refuse(req, socket, what, error);
|
|
179
|
+
server.emit = function emitThroughUpgradeRouter(event, ...args) {
|
|
180
|
+
if (event !== 'upgrade')
|
|
181
|
+
return emit.call(this, event, ...args);
|
|
182
|
+
const [req, socket, head] = args;
|
|
183
|
+
let route;
|
|
184
|
+
try {
|
|
185
|
+
route = routeFor(req);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
return refuse(req, socket, 'could not be routed', error);
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
if (!route)
|
|
192
|
+
return emit.call(this, event, ...args);
|
|
193
|
+
route.handle(req, socket, head);
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
return refuse(req, socket, `failed in the ${route ? route.name : 'Appium'} handler`, error);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
exports.UpgradeRouter = UpgradeRouter;
|
|
203
|
+
const routers = new WeakMap();
|
|
204
|
+
/**
|
|
205
|
+
* The server's router, installed on first use. ServerManager installs it in
|
|
206
|
+
* registerRoutes; attaching a WebSocket installs it on a server that has none.
|
|
207
|
+
*/
|
|
208
|
+
function upgradeRouterFor(server) {
|
|
209
|
+
let router = routers.get(server);
|
|
210
|
+
if (!router) {
|
|
211
|
+
router = new UpgradeRouter(server);
|
|
212
|
+
routers.set(server, router);
|
|
213
|
+
}
|
|
214
|
+
return router;
|
|
215
|
+
}
|
|
@@ -225,9 +225,10 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
225
225
|
// Principal Resource Management: Unblock device immediately
|
|
226
226
|
const device = this.sessionToDevice.get(sessionId);
|
|
227
227
|
if (device) {
|
|
228
|
-
const {
|
|
228
|
+
const { releaseSessionDevice } = yield Promise.resolve().then(() => __importStar(require('../data-service/device-service')));
|
|
229
229
|
try {
|
|
230
|
-
|
|
230
|
+
// This session's claim only: the phone may have gone on to another.
|
|
231
|
+
yield releaseSessionDevice(device.udid, device.host, sessionId);
|
|
231
232
|
logger_1.default.info(`🔓 [${sessionId}] Device ${device.udid} released.`);
|
|
232
233
|
}
|
|
233
234
|
catch (unblockErr) {
|
|
@@ -239,9 +240,9 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
239
240
|
yield this.stopIdleStreamForDevice(device);
|
|
240
241
|
}
|
|
241
242
|
else {
|
|
242
|
-
const {
|
|
243
|
+
const { releaseSessionDevices } = yield Promise.resolve().then(() => __importStar(require('../data-service/device-service')));
|
|
243
244
|
try {
|
|
244
|
-
yield
|
|
245
|
+
yield releaseSessionDevices(sessionId);
|
|
245
246
|
logger_1.default.info(`🔓 [${sessionId}] Device released via session_id fallback.`);
|
|
246
247
|
}
|
|
247
248
|
catch (unblockErr) {
|
|
@@ -261,9 +262,9 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
261
262
|
else {
|
|
262
263
|
logger_1.default.warn(`⚠️ Session ${sessionId} not found in SESSION_MANAGER`);
|
|
263
264
|
// Fallback: If session not in manager, attempt to unblock by session_id in store
|
|
264
|
-
const {
|
|
265
|
+
const { releaseSessionDevices } = yield Promise.resolve().then(() => __importStar(require('../data-service/device-service')));
|
|
265
266
|
try {
|
|
266
|
-
yield
|
|
267
|
+
yield releaseSessionDevices(sessionId);
|
|
267
268
|
logger_1.default.info(`🔓 [${sessionId}] Orphaned device released via session_id fallback.`);
|
|
268
269
|
}
|
|
269
270
|
catch (unblockErr) {
|
|
@@ -13,7 +13,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
exports.removeDevice = removeDevice;
|
|
16
|
-
exports.
|
|
16
|
+
exports.removeNodeDevices = removeNodeDevices;
|
|
17
17
|
exports.addNewDevice = addNewDevice;
|
|
18
18
|
exports.syncDiscoveredDevices = syncDiscoveredDevices;
|
|
19
19
|
exports.setSimulatorState = setSimulatorState;
|
|
@@ -23,6 +23,7 @@ exports.filterRowsByVisibleDevice = filterRowsByVisibleDevice;
|
|
|
23
23
|
exports.getDevices = getDevices;
|
|
24
24
|
exports.getDevice = getDevice;
|
|
25
25
|
exports.updatedAllocatedDevice = updatedAllocatedDevice;
|
|
26
|
+
exports.claimDeviceForSession = claimDeviceForSession;
|
|
26
27
|
exports.updateDeviceProgress = updateDeviceProgress;
|
|
27
28
|
exports.updateCmdExecutedTime = updateCmdExecutedTime;
|
|
28
29
|
exports.userBlockDevice = userBlockDevice;
|
|
@@ -30,6 +31,9 @@ exports.userUnblockDevice = userUnblockDevice;
|
|
|
30
31
|
exports.blockDevice = blockDevice;
|
|
31
32
|
exports.unblockDevice = unblockDevice;
|
|
32
33
|
exports.unblockDeviceMatchingFilter = unblockDeviceMatchingFilter;
|
|
34
|
+
exports.releaseSessionDevices = releaseSessionDevices;
|
|
35
|
+
exports.releaseSessionDevice = releaseSessionDevice;
|
|
36
|
+
exports.releasePendingClaim = releasePendingClaim;
|
|
33
37
|
exports.reserveDevice = reserveDevice;
|
|
34
38
|
exports.releaseReservation = releaseReservation;
|
|
35
39
|
exports.isDeviceReserved = isDeviceReserved;
|
|
@@ -43,6 +47,7 @@ const typedi_1 = require("typedi");
|
|
|
43
47
|
const CircuitBreaker_1 = require("./CircuitBreaker");
|
|
44
48
|
const NotificationService_1 = require("../services/NotificationService");
|
|
45
49
|
const deviceFieldOwners_1 = require("./deviceFieldOwners");
|
|
50
|
+
const deviceClaims_1 = require("./deviceClaims");
|
|
46
51
|
const SocketServer_1 = require("../services/SocketServer");
|
|
47
52
|
const prisma_1 = require("../prisma");
|
|
48
53
|
const manualLock_1 = require("../services/recording/manualLock");
|
|
@@ -63,8 +68,8 @@ const store = new Proxy({}, {
|
|
|
63
68
|
function deviceScope(udid, teamId) {
|
|
64
69
|
return teamId === undefined ? { udid } : { udid, teamId };
|
|
65
70
|
}
|
|
66
|
-
function removeDevice(
|
|
67
|
-
return __awaiter(this,
|
|
71
|
+
function removeDevice(devices_1) {
|
|
72
|
+
return __awaiter(this, arguments, void 0, function* (devices, options = {}) {
|
|
68
73
|
for (const device of devices) {
|
|
69
74
|
logger_1.default.info(`Removing device ${device.udid} from host ${device.host}`);
|
|
70
75
|
const socket = typedi_1.Container.get(SocketServer_1.SocketServer);
|
|
@@ -75,18 +80,44 @@ function removeDevice(devices) {
|
|
|
75
80
|
const team = socket.hasScopedDashboard()
|
|
76
81
|
? yield typedi_1.Container.get(DeviceTeamResolver_1.DeviceTeamResolver).resolve(device.udid)
|
|
77
82
|
: undefined;
|
|
78
|
-
yield store.removeDevices({ udid: device.udid, host: device.host });
|
|
83
|
+
yield store.removeDevices({ udid: device.udid, host: device.host }, options);
|
|
79
84
|
typedi_1.Container.get(NotificationService_1.NotificationService).dispatchEvent('device_offline', device);
|
|
80
85
|
void socket.emitToDashboardForDevices('device_removed', device, deviceScope(device.udid, (team === null || team === void 0 ? void 0 : team.known) ? team.teamId : undefined));
|
|
81
86
|
}
|
|
82
87
|
});
|
|
83
88
|
}
|
|
84
|
-
|
|
89
|
+
/**
|
|
90
|
+
* The phones a node asks the hub to forget (`POST /register`, `type=remove`
|
|
91
|
+
* for one phone, `type=unregister` when it shuts down). Only that node's: the
|
|
92
|
+
* rows its `nodeId` names when it sends one, else those filed under exactly
|
|
93
|
+
* its `host`, and never one of this server's own (`isOwn`). A udid-less
|
|
94
|
+
* `remove` names no phone and takes nothing.
|
|
95
|
+
*
|
|
96
|
+
* The store matched a host that isn't a URL as a substring, and a remove with
|
|
97
|
+
* no host by udid alone, so a node (an older one sends a bare IP) could delete
|
|
98
|
+
* the hub's own phones, or another node's. Returns how many rows went.
|
|
99
|
+
*/
|
|
100
|
+
function removeNodeDevices(removals, isOwn) {
|
|
85
101
|
return __awaiter(this, void 0, void 0, function* () {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
102
|
+
const all = yield store.getAllDevices();
|
|
103
|
+
const gone = new Map();
|
|
104
|
+
for (const removal of removals) {
|
|
105
|
+
if (!removal.nodeId && !removal.host)
|
|
106
|
+
continue;
|
|
107
|
+
for (const device of all) {
|
|
108
|
+
if (isOwn(device))
|
|
109
|
+
continue;
|
|
110
|
+
if (removal.udid !== undefined && device.udid !== removal.udid)
|
|
111
|
+
continue;
|
|
112
|
+
const theirs = removal.nodeId
|
|
113
|
+
? device.nodeId === removal.nodeId
|
|
114
|
+
: device.host === removal.host;
|
|
115
|
+
if (theirs)
|
|
116
|
+
gone.set(`${device.udid}\u0000${device.host}`, device);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
yield removeDevice([...gone.values()].map((d) => ({ udid: d.udid, host: d.host })), { exactHost: true });
|
|
120
|
+
return gone.size;
|
|
90
121
|
});
|
|
91
122
|
}
|
|
92
123
|
function addNewDevice(devices_1, host_1) {
|
|
@@ -237,6 +268,22 @@ function updatedAllocatedDevice(device, updateData) {
|
|
|
237
268
|
yield store.updateDevice(device.udid, device.host, updateData);
|
|
238
269
|
});
|
|
239
270
|
}
|
|
271
|
+
/**
|
|
272
|
+
* Put a created session on the claim its phone was allocated with
|
|
273
|
+
* (`device.claimedAt`), writing `updateData` with it. False, and nothing
|
|
274
|
+
* written, when another session holds the phone now: its claim is not
|
|
275
|
+
* overwritten.
|
|
276
|
+
*/
|
|
277
|
+
function claimDeviceForSession(device, sessionId, updateData) {
|
|
278
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
279
|
+
const claimed = yield store.claimForSession(device.udid, device.host, device.claimedAt, sessionId, updateData);
|
|
280
|
+
if (!claimed) {
|
|
281
|
+
logger_1.default.error(`Session ${sessionId} was created on ${device.udid} at ${device.host}, but another ` +
|
|
282
|
+
'session has claimed the device since. Leaving that claim in place.');
|
|
283
|
+
}
|
|
284
|
+
return claimed;
|
|
285
|
+
});
|
|
286
|
+
}
|
|
240
287
|
function updateDeviceProgress(udid_1, host_1, progress_1) {
|
|
241
288
|
return __awaiter(this, arguments, void 0, function* (udid, host, progress, extra = {}) {
|
|
242
289
|
logger_1.default.debug(`[${udid}] progress: ${progress}`);
|
|
@@ -249,9 +296,9 @@ function updateDeviceProgress(udid_1, host_1, progress_1) {
|
|
|
249
296
|
}
|
|
250
297
|
function updateCmdExecutedTime(sessionId) {
|
|
251
298
|
return __awaiter(this, void 0, void 0, function* () {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
299
|
+
// One conditional write. Reading the row and writing all of it back would
|
|
300
|
+
// put back a claim released, or a node's report taken, in between.
|
|
301
|
+
yield store.touchSession(sessionId, new Date().getTime());
|
|
255
302
|
});
|
|
256
303
|
}
|
|
257
304
|
/**
|
|
@@ -297,53 +344,109 @@ function blockDevice(udid, host, sessionId) {
|
|
|
297
344
|
}, { udid });
|
|
298
345
|
});
|
|
299
346
|
}
|
|
347
|
+
/**
|
|
348
|
+
* Free a phone whatever holds it: a manual hold (a preview, a recording) or
|
|
349
|
+
* an admin's release. A session's phone is released with
|
|
350
|
+
* releaseSessionDevices / releaseSessionDevice / releasePendingClaim instead,
|
|
351
|
+
* keyed on its claim.
|
|
352
|
+
*
|
|
353
|
+
* With a host, only that row. A hub also stores its nodes' phones, and a node
|
|
354
|
+
* on the same machine lists the same udid under its own host: matching the
|
|
355
|
+
* udid alone freed that row too, with a session on it.
|
|
356
|
+
*/
|
|
300
357
|
function unblockDevice(udid, host) {
|
|
301
358
|
return __awaiter(this, void 0, void 0, function* () {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
359
|
+
const devices = yield store.findDevices(host === undefined ? { udid } : { udid, host });
|
|
360
|
+
yield forceRelease(devices);
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
/** unblockDevice for every phone matching `filter` (getDevices' filter). */
|
|
364
|
+
function unblockDeviceMatchingFilter(filter) {
|
|
365
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
366
|
+
yield forceRelease(yield store.getDevices(filter));
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
function forceRelease(devices) {
|
|
370
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
371
|
+
yield Promise.all(devices.map((device) => __awaiter(this, void 0, void 0, function* () {
|
|
372
|
+
const totalUtilization = utilizationAfter(device);
|
|
373
|
+
yield (0, device_utils_1.setUtilizationTime)(device.udid, totalUtilization);
|
|
374
|
+
yield store.updateDevice(device.udid, device.host, Object.assign(Object.assign({}, deviceClaims_1.CLAIM_RESET), { busy: false, totalUtilizationTimeMilliSec: totalUtilization }));
|
|
375
|
+
logger_1.default.debug(`Unblocked device ${device.udid}`);
|
|
376
|
+
emitUnblocked(device);
|
|
377
|
+
}))).catch((error) => {
|
|
378
|
+
logger_1.default.error(`Unable to unblock device: ${error}`);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* End a session's claim on its phone, keyed on the session's id: a release
|
|
384
|
+
* for a session that has ended, arriving after its phone went to another one,
|
|
385
|
+
* matches nothing. The phone stays busy while its node still reports it busy
|
|
386
|
+
* (deviceClaims.ts). Never throws.
|
|
387
|
+
*/
|
|
388
|
+
function releaseSessionDevices(sessionId) {
|
|
389
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
390
|
+
try {
|
|
391
|
+
const rows = new Map();
|
|
392
|
+
for (const device of [
|
|
393
|
+
...(yield store.findDevices({ claimSessionId: sessionId })),
|
|
394
|
+
...(yield store.findDevices({ session_id: sessionId })),
|
|
395
|
+
]) {
|
|
396
|
+
rows.set(`${device.udid}@${device.host}`, device);
|
|
397
|
+
}
|
|
398
|
+
yield Promise.all([...rows.values()].map((device) => releaseClaimOn(device, { sessionId })));
|
|
309
399
|
}
|
|
310
|
-
|
|
311
|
-
|
|
400
|
+
catch (error) {
|
|
401
|
+
logger_1.default.error(`Unable to release the device of session ${sessionId}: ${error}`);
|
|
312
402
|
}
|
|
313
403
|
});
|
|
314
404
|
}
|
|
315
|
-
|
|
405
|
+
/** releaseSessionDevices for one phone. */
|
|
406
|
+
function releaseSessionDevice(udid, host, sessionId) {
|
|
316
407
|
return __awaiter(this, void 0, void 0, function* () {
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
});
|
|
408
|
+
const device = yield store.findDevice({ udid, host });
|
|
409
|
+
return device ? releaseClaimOn(device, { sessionId }) : false;
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Give back a phone allocated for a session that was never created: the
|
|
414
|
+
* pending claim it was allocated with (`claimedAt`, as the allocation read
|
|
415
|
+
* it). If that claim has gone (timed out, and the phone taken since) nothing
|
|
416
|
+
* is written.
|
|
417
|
+
*/
|
|
418
|
+
function releasePendingClaim(allocated) {
|
|
419
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
420
|
+
var _a;
|
|
421
|
+
const device = yield store.findDevice({ udid: allocated.udid, host: allocated.host });
|
|
422
|
+
return device ? releaseClaimOn(device, { claimedAt: (_a = allocated.claimedAt) !== null && _a !== void 0 ? _a : null }) : false;
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
function releaseClaimOn(device, ref) {
|
|
426
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
427
|
+
const totalUtilization = utilizationAfter(device);
|
|
428
|
+
const released = yield store.releaseClaim(device.udid, device.host, ref, {
|
|
429
|
+
totalUtilizationTimeMilliSec: totalUtilization,
|
|
430
|
+
});
|
|
431
|
+
if (!released) {
|
|
432
|
+
logger_1.default.info(`Not releasing ${device.udid} at ${device.host}: it no longer holds ${JSON.stringify(ref)}`);
|
|
433
|
+
return false;
|
|
344
434
|
}
|
|
435
|
+
yield (0, device_utils_1.setUtilizationTime)(device.udid, totalUtilization);
|
|
436
|
+
logger_1.default.debug(`Released ${JSON.stringify(ref)} on ${device.udid}`);
|
|
437
|
+
emitUnblocked(device);
|
|
438
|
+
return true;
|
|
345
439
|
});
|
|
346
440
|
}
|
|
441
|
+
/** The phone's total use once the session on it now ends. */
|
|
442
|
+
function utilizationAfter(device) {
|
|
443
|
+
var _a;
|
|
444
|
+
const started = device.sessionStartTime;
|
|
445
|
+
return ((_a = device.totalUtilizationTimeMilliSec) !== null && _a !== void 0 ? _a : 0) + (started ? Date.now() - started : 0);
|
|
446
|
+
}
|
|
447
|
+
function emitUnblocked(device) {
|
|
448
|
+
void typedi_1.Container.get(SocketServer_1.SocketServer).emitToDashboardForDevices('device_unblocked', { udid: device.udid, host: device.host }, deviceScope(device.udid, device.teamId));
|
|
449
|
+
}
|
|
347
450
|
/**
|
|
348
451
|
* Reserve a device for exclusive manual use
|
|
349
452
|
* @param udid Device UDID
|