@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
|
@@ -13,6 +13,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
exports.DeviceStoreFactory = void 0;
|
|
16
|
+
const deviceFieldOwners_1 = require("./deviceFieldOwners");
|
|
17
|
+
const deviceClaims_1 = require("./deviceClaims");
|
|
16
18
|
const logger_1 = __importDefault(require("../logger"));
|
|
17
19
|
const semver_1 = __importDefault(require("semver"));
|
|
18
20
|
const db_1 = require("./db");
|
|
@@ -153,19 +155,37 @@ class LokiDeviceStore {
|
|
|
153
155
|
});
|
|
154
156
|
});
|
|
155
157
|
}
|
|
156
|
-
|
|
158
|
+
touchSession(sessionId, at) {
|
|
157
159
|
return __awaiter(this, void 0, void 0, function* () {
|
|
158
|
-
(yield db_1.XenonDatabase.DeviceModel)
|
|
160
|
+
(yield db_1.XenonDatabase.DeviceModel)
|
|
161
|
+
.chain()
|
|
162
|
+
.find({ session_id: sessionId })
|
|
163
|
+
.update((device) => {
|
|
164
|
+
device.lastCmdExecutedAt = at;
|
|
165
|
+
});
|
|
159
166
|
});
|
|
160
167
|
}
|
|
161
|
-
addDevices(
|
|
162
|
-
return __awaiter(this,
|
|
168
|
+
addDevices(devices_1) {
|
|
169
|
+
return __awaiter(this, arguments, void 0, function* (devices, options = {}) {
|
|
163
170
|
const deviceModel = yield db_1.XenonDatabase.DeviceModel;
|
|
164
171
|
const added = [];
|
|
165
172
|
for (const device of devices) {
|
|
166
173
|
const existing = deviceModel.findOne({ udid: device.udid, host: device.host });
|
|
174
|
+
// A node's report (PrismaDeviceStore.addNodeReport has the rule).
|
|
175
|
+
const nodeBusy = device.busy === true;
|
|
176
|
+
if (existing && options.nodeReport) {
|
|
177
|
+
Object.assign(existing, (0, deviceFieldOwners_1.pickNodeReportFields)(device), { nodeBusy });
|
|
178
|
+
if (nodeBusy)
|
|
179
|
+
existing.busy = true;
|
|
180
|
+
else if ((0, deviceClaims_1.isUnheld)(existing))
|
|
181
|
+
existing.busy = false;
|
|
182
|
+
deviceModel.update(existing);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
167
185
|
if (!existing) {
|
|
168
|
-
const cleanDevice =
|
|
186
|
+
const cleanDevice = options.nodeReport
|
|
187
|
+
? Object.assign(Object.assign({}, (0, deviceFieldOwners_1.pickNodeReportFields)(device)), { udid: device.udid, host: device.host, busy: nodeBusy, nodeBusy })
|
|
188
|
+
: Object.assign({}, device);
|
|
169
189
|
if (cleanDevice.host === undefined)
|
|
170
190
|
cleanDevice.host = 'Local';
|
|
171
191
|
if (cleanDevice.userBlocked === undefined)
|
|
@@ -185,14 +205,24 @@ class LokiDeviceStore {
|
|
|
185
205
|
return added;
|
|
186
206
|
});
|
|
187
207
|
}
|
|
208
|
+
// Loki's find() matches a host exactly already, so `exactHost` changes nothing here.
|
|
188
209
|
removeDevices(filter) {
|
|
189
210
|
return __awaiter(this, void 0, void 0, function* () {
|
|
190
211
|
(yield db_1.XenonDatabase.DeviceModel).chain().find(filter).remove();
|
|
191
212
|
});
|
|
192
213
|
}
|
|
193
|
-
clearStorage() {
|
|
214
|
+
clearStorage(onlyHosts) {
|
|
194
215
|
return __awaiter(this, void 0, void 0, function* () {
|
|
195
|
-
|
|
216
|
+
const model = yield db_1.XenonDatabase.DeviceModel;
|
|
217
|
+
if (onlyHosts) {
|
|
218
|
+
model
|
|
219
|
+
.chain()
|
|
220
|
+
.find({ host: { $in: [...onlyHosts] } })
|
|
221
|
+
.remove();
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
model.removeDataOnly();
|
|
225
|
+
}
|
|
196
226
|
});
|
|
197
227
|
}
|
|
198
228
|
findDevice(filter) {
|
|
@@ -205,8 +235,8 @@ class LokiDeviceStore {
|
|
|
205
235
|
return (yield db_1.XenonDatabase.DeviceModel).find(filter);
|
|
206
236
|
});
|
|
207
237
|
}
|
|
208
|
-
findAndLockDevice(
|
|
209
|
-
return __awaiter(this,
|
|
238
|
+
findAndLockDevice(filterOptions_1) {
|
|
239
|
+
return __awaiter(this, arguments, void 0, function* (filterOptions, options = {}) {
|
|
210
240
|
// Phase 2: exclude devices held by an active lease.
|
|
211
241
|
const activeLeases = yield prisma_1.prisma.lease.findMany({
|
|
212
242
|
where: { status: 'active' },
|
|
@@ -216,12 +246,53 @@ class LokiDeviceStore {
|
|
|
216
246
|
const devices = yield this.getDevices(filterOptions);
|
|
217
247
|
const available = devices.find((d) => !d.busy && !d.userBlocked && !blockedKeys.has(`${d.udid}@${d.host}`));
|
|
218
248
|
if (available) {
|
|
219
|
-
yield this.updateDevice(available.udid, available.host, { busy: true });
|
|
249
|
+
yield this.updateDevice(available.udid, available.host, Object.assign({ busy: true }, (options.claim ? { claimedAt: Date.now(), claimSessionId: null } : {})));
|
|
220
250
|
return available;
|
|
221
251
|
}
|
|
222
252
|
return null;
|
|
223
253
|
});
|
|
224
254
|
}
|
|
255
|
+
claimForSession(udid, host, claimedAt, sessionId, update) {
|
|
256
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
257
|
+
const model = yield db_1.XenonDatabase.DeviceModel;
|
|
258
|
+
const device = model.findOne({ udid, host });
|
|
259
|
+
if (!device || device.claimSessionId != null)
|
|
260
|
+
return false;
|
|
261
|
+
if (device.claimedAt != null && device.claimedAt !== claimedAt)
|
|
262
|
+
return false;
|
|
263
|
+
Object.assign(device, update, {
|
|
264
|
+
busy: true,
|
|
265
|
+
claimSessionId: sessionId,
|
|
266
|
+
claimedAt: claimedAt !== null && claimedAt !== void 0 ? claimedAt : Date.now(),
|
|
267
|
+
});
|
|
268
|
+
model.update(device);
|
|
269
|
+
return true;
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
releaseClaim(udid, host, ref, update) {
|
|
273
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
274
|
+
const model = yield db_1.XenonDatabase.DeviceModel;
|
|
275
|
+
const device = model.findOne({ udid, host });
|
|
276
|
+
if (!device || !(0, deviceClaims_1.holdsClaim)(device, ref))
|
|
277
|
+
return false;
|
|
278
|
+
Object.assign(device, update, deviceClaims_1.CLAIM_RESET);
|
|
279
|
+
if ((0, deviceClaims_1.isUnheld)(device))
|
|
280
|
+
device.busy = false;
|
|
281
|
+
model.update(device);
|
|
282
|
+
return true;
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
releaseLeaseLock(udid, host) {
|
|
286
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
287
|
+
const model = yield db_1.XenonDatabase.DeviceModel;
|
|
288
|
+
const device = model.findOne({ udid, host });
|
|
289
|
+
if (!(device === null || device === void 0 ? void 0 : device.busy) || !(0, deviceClaims_1.isUnheld)(device))
|
|
290
|
+
return false;
|
|
291
|
+
device.busy = false;
|
|
292
|
+
model.update(device);
|
|
293
|
+
return true;
|
|
294
|
+
});
|
|
295
|
+
}
|
|
225
296
|
resetMetrics() {
|
|
226
297
|
return __awaiter(this, void 0, void 0, function* () {
|
|
227
298
|
(yield db_1.XenonDatabase.DeviceModel)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CLAIM_RESET = exports.UNHELD = exports.NO_CLAIM = exports.PENDING_CLAIM_TIMEOUT_MS = void 0;
|
|
4
|
+
exports.isPendingClaim = isPendingClaim;
|
|
5
|
+
exports.pendingClaimExpired = pendingClaimExpired;
|
|
6
|
+
exports.claimWhere = claimWhere;
|
|
7
|
+
exports.holdsClaim = holdsClaim;
|
|
8
|
+
exports.hasNoClaim = hasNoClaim;
|
|
9
|
+
exports.isUnheld = isUnheld;
|
|
10
|
+
/**
|
|
11
|
+
* Why a phone is busy, on the server that allocates it.
|
|
12
|
+
*
|
|
13
|
+
* Allocation reads `busy` (findAndLockDevice takes a phone only where it is
|
|
14
|
+
* false, in one conditional update). On a hub a node's phone is busy for one
|
|
15
|
+
* of two reasons, each kept in its own columns so neither can erase the
|
|
16
|
+
* other:
|
|
17
|
+
*
|
|
18
|
+
* - **The claim**, this server's: `claimedAt` (when allocation took the phone)
|
|
19
|
+
* and `claimSessionId` (the session's id, written when the session exists).
|
|
20
|
+
* A claim with no session id yet is *pending*: its session is being
|
|
21
|
+
* created, which can take minutes on a first driver install.
|
|
22
|
+
* - **The node's report**: `nodeBusy`, what the node last said
|
|
23
|
+
* (`POST /xenon/api/register`). Always false for a phone this server drives.
|
|
24
|
+
*
|
|
25
|
+
* `busy` is claim OR report. No write that can clear it reads the row first:
|
|
26
|
+
*
|
|
27
|
+
* - A node reporting its phone free clears `busy` only where nothing of the
|
|
28
|
+
* hub's holds it (UNHELD: no claim, and no hold such as a preview or a
|
|
29
|
+
* recording in `session_id`), in one conditional update. A report sent
|
|
30
|
+
* before the hub's claim (every 30 s by default) can no longer undo it.
|
|
31
|
+
* - A release names the claim it ends (ClaimRef): a session's id, or when a
|
|
32
|
+
* pending claim was taken. A stale release, for a session that ended while
|
|
33
|
+
* the phone went on to another, matches nothing. Then `busy` is cleared
|
|
34
|
+
* only where nothing else holds the phone (UNHELD), again one conditional
|
|
35
|
+
* update: a node still reporting it busy keeps it busy.
|
|
36
|
+
* - The idle sweeper leaves a pending claim alone until
|
|
37
|
+
* PENDING_CLAIM_TIMEOUT_MS, whatever the new-command timeout.
|
|
38
|
+
*
|
|
39
|
+
* A server with no nodes (standalone, or a node itself) never has `nodeBusy`
|
|
40
|
+
* set, so `busy` is its claim as it always was.
|
|
41
|
+
*
|
|
42
|
+
* Only a session's allocation takes a claim. A lease locks with `busy` alone,
|
|
43
|
+
* as before; a manual hold (a preview, a recording) writes `session_id`.
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* How long a claim may wait for its session to be created before the idle
|
|
47
|
+
* sweeper frees it: a create that never finished (the hub restarted during
|
|
48
|
+
* it, or it hung). Well past a create's own budget: the phone's wait
|
|
49
|
+
* (`deviceAvailabilityTimeoutMs`) comes before the claim, and a first
|
|
50
|
+
* UiAutomator2 or WebDriverAgent install fits in a few minutes.
|
|
51
|
+
*/
|
|
52
|
+
exports.PENDING_CLAIM_TIMEOUT_MS = 10 * 60000;
|
|
53
|
+
/** A claim whose session is still being created. */
|
|
54
|
+
function isPendingClaim(device) {
|
|
55
|
+
return device.claimedAt != null && device.claimSessionId == null;
|
|
56
|
+
}
|
|
57
|
+
/** A pending claim older than PENDING_CLAIM_TIMEOUT_MS. */
|
|
58
|
+
function pendingClaimExpired(device, now) {
|
|
59
|
+
return isPendingClaim(device) && now - device.claimedAt > exports.PENDING_CLAIM_TIMEOUT_MS;
|
|
60
|
+
}
|
|
61
|
+
/** Prisma `where` (beside udid and host) for the claim `ref` names. */
|
|
62
|
+
function claimWhere(ref) {
|
|
63
|
+
if ('sessionId' in ref) {
|
|
64
|
+
return {
|
|
65
|
+
OR: [{ claimSessionId: ref.sessionId }, { claimSessionId: null, session_id: ref.sessionId }],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (ref.claimedAt == null)
|
|
69
|
+
return { claimSessionId: null, claimedAt: null, session_id: null };
|
|
70
|
+
return { claimSessionId: null, claimedAt: ref.claimedAt };
|
|
71
|
+
}
|
|
72
|
+
/** claimWhere as a predicate, for the in-memory store. */
|
|
73
|
+
function holdsClaim(device, ref) {
|
|
74
|
+
if ('sessionId' in ref) {
|
|
75
|
+
return (device.claimSessionId === ref.sessionId ||
|
|
76
|
+
(device.claimSessionId == null && device.session_id === ref.sessionId));
|
|
77
|
+
}
|
|
78
|
+
if (device.claimSessionId != null)
|
|
79
|
+
return false;
|
|
80
|
+
if (ref.claimedAt == null)
|
|
81
|
+
return device.claimedAt == null && device.session_id == null;
|
|
82
|
+
return device.claimedAt === ref.claimedAt;
|
|
83
|
+
}
|
|
84
|
+
/** No claim of this server's on the phone, pending or not. */
|
|
85
|
+
exports.NO_CLAIM = { claimSessionId: null, claimedAt: null };
|
|
86
|
+
function hasNoClaim(device) {
|
|
87
|
+
return device.claimSessionId == null && device.claimedAt == null;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Nothing holds the phone: no claim, no session or manual hold, and no node
|
|
91
|
+
* reporting it busy. Only then may a release clear `busy`.
|
|
92
|
+
*/
|
|
93
|
+
exports.UNHELD = Object.assign(Object.assign({}, exports.NO_CLAIM), { session_id: null, nodeBusy: false });
|
|
94
|
+
function isUnheld(device) {
|
|
95
|
+
return hasNoClaim(device) && device.session_id == null && device.nodeBusy !== true;
|
|
96
|
+
}
|
|
97
|
+
/** What ending a claim resets: the claim, and the session's bookkeeping. */
|
|
98
|
+
exports.CLAIM_RESET = Object.assign(Object.assign({}, exports.NO_CLAIM), { session_id: null, lastCmdExecutedAt: null, sessionStartTime: 0, newCommandTimeout: null, sessionProgress: '', owningSessionId: null, lockedAt: null });
|
|
@@ -3,8 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.DISCOVERY_FIELDS = void 0;
|
|
6
|
+
exports.NODE_REPORT_FIELDS = exports.DISCOVERY_FIELDS = void 0;
|
|
7
7
|
exports.pickDiscoveryFields = pickDiscoveryFields;
|
|
8
|
+
exports.pickNodeReportFields = pickNodeReportFields;
|
|
8
9
|
exports.discoveryChanges = discoveryChanges;
|
|
9
10
|
const lodash_1 = __importDefault(require("lodash"));
|
|
10
11
|
/**
|
|
@@ -12,12 +13,21 @@ const lodash_1 = __importDefault(require("lodash"));
|
|
|
12
13
|
*
|
|
13
14
|
* The periodic device sync reads every row, spends seconds in adb or simctl,
|
|
14
15
|
* then writes each phone back. For a phone it already knows it may write only
|
|
15
|
-
* what discovery itself observes (`discovery`). Every
|
|
16
|
+
* what discovery itself observes (`discovery`). Every other column has
|
|
16
17
|
* another writer: a session's lock, a preview's hold, an admin's block,
|
|
17
18
|
* reservation or team, a counter, the health check. A sync that read the row
|
|
18
19
|
* before one of those writes would put the old value back. That erased a live
|
|
19
20
|
* preview hold on the lab, and could hand a phone in a session to a second one.
|
|
20
21
|
*
|
|
22
|
+
* A hub also stores its nodes' phones, from each node's report
|
|
23
|
+
* (`POST /xenon/api/register`, NODE_REPORT_FIELDS). The report carries what
|
|
24
|
+
* the node observes of its own phone: `discovery`, and `observed` (its health,
|
|
25
|
+
* which only the server driving the phone can check). It never writes a
|
|
26
|
+
* `runtime` column: the hub's claim for a session, and the settings the hub
|
|
27
|
+
* owns for the phone (team, tags, reservation, block). Whether the node says
|
|
28
|
+
* the phone is busy goes to `nodeBusy` (`report`), never to `busy`
|
|
29
|
+
* (deviceClaims.ts).
|
|
30
|
+
*
|
|
21
31
|
* Typed over every column, so a new column won't compile until it is placed.
|
|
22
32
|
*/
|
|
23
33
|
const OWNER = {
|
|
@@ -59,8 +69,9 @@ const OWNER = {
|
|
|
59
69
|
newCommandTimeout: 'runtime',
|
|
60
70
|
owningSessionId: 'runtime',
|
|
61
71
|
lockedAt: 'runtime',
|
|
72
|
+
claimSessionId: 'runtime',
|
|
73
|
+
claimedAt: 'runtime',
|
|
62
74
|
userBlocked: 'runtime',
|
|
63
|
-
offline: 'runtime',
|
|
64
75
|
reservationReason: 'runtime',
|
|
65
76
|
reservedBy: 'runtime',
|
|
66
77
|
reservedUntil: 'runtime',
|
|
@@ -68,14 +79,16 @@ const OWNER = {
|
|
|
68
79
|
tags: 'runtime',
|
|
69
80
|
totalUtilizationTimeMilliSec: 'runtime',
|
|
70
81
|
total_session_count: 'runtime',
|
|
71
|
-
totalHealedCount: 'runtime',
|
|
72
82
|
dashboard_link: 'runtime',
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
83
|
+
offline: 'observed',
|
|
84
|
+
totalHealedCount: 'observed',
|
|
85
|
+
healthStatus: 'observed',
|
|
86
|
+
healthCheckError: 'observed',
|
|
87
|
+
lastHealthCheckAt: 'observed',
|
|
88
|
+
batteryLevel: 'observed',
|
|
89
|
+
thermalStatus: 'observed',
|
|
90
|
+
storageFree: 'observed',
|
|
91
|
+
nodeBusy: 'report',
|
|
79
92
|
};
|
|
80
93
|
/** The columns a sync may write to a phone it already knows. */
|
|
81
94
|
exports.DISCOVERY_FIELDS = new Set(Object.keys(OWNER).filter((column) => OWNER[column] === 'discovery'));
|
|
@@ -83,6 +96,15 @@ exports.DISCOVERY_FIELDS = new Set(Object.keys(OWNER).filter((column) => OWNER[c
|
|
|
83
96
|
function pickDiscoveryFields(data) {
|
|
84
97
|
return lodash_1.default.pickBy(data, (_value, key) => exports.DISCOVERY_FIELDS.has(key));
|
|
85
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* The columns a hub takes from a node's report of a phone: what the node
|
|
101
|
+
* observes of it. Not `nodeBusy`, which the hub sets from the report's `busy`.
|
|
102
|
+
*/
|
|
103
|
+
exports.NODE_REPORT_FIELDS = new Set(Object.keys(OWNER).filter((column) => OWNER[column] === 'discovery' || OWNER[column] === 'observed'));
|
|
104
|
+
/** Only the columns of `data` a hub takes from a node's report. */
|
|
105
|
+
function pickNodeReportFields(data) {
|
|
106
|
+
return lodash_1.default.pickBy(data, (_value, key) => exports.NODE_REPORT_FIELDS.has(key));
|
|
107
|
+
}
|
|
86
108
|
/**
|
|
87
109
|
* What a sync writes to a phone it already knows: the discovery columns whose
|
|
88
110
|
* value differs from `known`, the row the sync started from. Discovery hands
|
|
@@ -52,6 +52,7 @@ const semver = __importStar(require("semver"));
|
|
|
52
52
|
const logger_1 = __importDefault(require("../logger"));
|
|
53
53
|
const deviceColumns_1 = require("./deviceColumns");
|
|
54
54
|
const deviceFieldOwners_1 = require("./deviceFieldOwners");
|
|
55
|
+
const deviceClaims_1 = require("./deviceClaims");
|
|
55
56
|
/** Logged once per key, so a chatty node doesn't flood the log. */
|
|
56
57
|
const droppedDeviceKeys = new Set();
|
|
57
58
|
class PrismaDeviceStore {
|
|
@@ -75,8 +76,8 @@ class PrismaDeviceStore {
|
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
toIDevice(device) {
|
|
78
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
79
|
-
return Object.assign(Object.assign({}, device), { cloud: this.safeParse(device.cloud, 'cloud', device.udid), capability: this.safeParse(device.capability, 'capability', device.udid), chromeDriverPath: this.safeParse(device.chromeDriverPath, 'chromeDriverPath', device.udid), tags: this.safeParse(device.tags, 'tags', device.udid), platform: (device.platform || 'android'), name: device.name || 'unknown', state: device.state || 'available', sdk: device.sdk || 'unknown', deviceType: device.deviceType || 'real', busy: (_a = device.busy) !== null && _a !== void 0 ? _a : false, userBlocked: (_b = device.userBlocked) !== null && _b !== void 0 ? _b : false, realDevice: (_c = device.realDevice) !== null && _c !== void 0 ? _c : true, lastHealthCheckAt: (_d = device.lastHealthCheckAt) !== null && _d !== void 0 ? _d : undefined, healthStatus: (_e = device.healthStatus) !== null && _e !== void 0 ? _e : 'Healthy', healthCheckError: (_f = device.healthCheckError) !== null && _f !== void 0 ? _f : undefined, batteryLevel: (_g = device.batteryLevel) !== null && _g !== void 0 ? _g : undefined, thermalStatus: (_h = device.thermalStatus) !== null && _h !== void 0 ? _h : undefined, storageFree: (_j = device.storageFree) !== null && _j !== void 0 ? _j : undefined, sessionProgress: (_k = device.sessionProgress) !== null && _k !== void 0 ? _k : '', totalHealedCount: (_l = device.totalHealedCount) !== null && _l !== void 0 ? _l : 0 });
|
|
79
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
80
|
+
return Object.assign(Object.assign({}, device), { cloud: this.safeParse(device.cloud, 'cloud', device.udid), capability: this.safeParse(device.capability, 'capability', device.udid), chromeDriverPath: this.safeParse(device.chromeDriverPath, 'chromeDriverPath', device.udid), tags: this.safeParse(device.tags, 'tags', device.udid), platform: (device.platform || 'android'), name: device.name || 'unknown', state: device.state || 'available', sdk: device.sdk || 'unknown', deviceType: device.deviceType || 'real', busy: (_a = device.busy) !== null && _a !== void 0 ? _a : false, userBlocked: (_b = device.userBlocked) !== null && _b !== void 0 ? _b : false, realDevice: (_c = device.realDevice) !== null && _c !== void 0 ? _c : true, lastHealthCheckAt: (_d = device.lastHealthCheckAt) !== null && _d !== void 0 ? _d : undefined, healthStatus: (_e = device.healthStatus) !== null && _e !== void 0 ? _e : 'Healthy', healthCheckError: (_f = device.healthCheckError) !== null && _f !== void 0 ? _f : undefined, batteryLevel: (_g = device.batteryLevel) !== null && _g !== void 0 ? _g : undefined, thermalStatus: (_h = device.thermalStatus) !== null && _h !== void 0 ? _h : undefined, storageFree: (_j = device.storageFree) !== null && _j !== void 0 ? _j : undefined, sessionProgress: (_k = device.sessionProgress) !== null && _k !== void 0 ? _k : '', totalHealedCount: (_l = device.totalHealedCount) !== null && _l !== void 0 ? _l : 0, nodeBusy: (_m = device.nodeBusy) !== null && _m !== void 0 ? _m : false });
|
|
80
81
|
}
|
|
81
82
|
fromIDevice(device) {
|
|
82
83
|
const data = Object.assign({}, device);
|
|
@@ -208,17 +209,12 @@ class PrismaDeviceStore {
|
|
|
208
209
|
logger_1.default.debug(`[PrismaStore] Update device ${udid} at ${host}: ${result.count} records affected`);
|
|
209
210
|
});
|
|
210
211
|
}
|
|
211
|
-
|
|
212
|
+
touchSession(sessionId, at) {
|
|
212
213
|
return __awaiter(this, void 0, void 0, function* () {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
yield this.prisma.device.update({
|
|
218
|
-
where: { udid_host: { udid: d.udid, host: d.host } },
|
|
219
|
-
data: this.fromIDevice(idv),
|
|
220
|
-
});
|
|
221
|
-
}
|
|
214
|
+
yield this.prisma.device.updateMany({
|
|
215
|
+
where: { session_id: sessionId },
|
|
216
|
+
data: { lastCmdExecutedAt: at },
|
|
217
|
+
});
|
|
222
218
|
});
|
|
223
219
|
}
|
|
224
220
|
addDevices(devices_1) {
|
|
@@ -232,31 +228,63 @@ class PrismaDeviceStore {
|
|
|
232
228
|
const known = new Set(existing.map((d) => `${d.udid}@${d.host}`));
|
|
233
229
|
const added = [];
|
|
234
230
|
for (const device of devices) {
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
231
|
+
const d = options.nodeReport
|
|
232
|
+
? yield this.addNodeReport(device)
|
|
233
|
+
: yield this.prisma.device.upsert({
|
|
234
|
+
// Use upsert to avoid race conditions and unique constraint errors. A
|
|
235
|
+
// phone already here gets only its discovery columns (AddDevicesOptions).
|
|
236
|
+
where: { udid_host: { udid: device.udid, host: device.host } },
|
|
237
|
+
update: (0, deviceFieldOwners_1.pickDiscoveryFields)(this.fromIDevice(device)),
|
|
238
|
+
create: this.fromIDevice(device),
|
|
239
|
+
});
|
|
243
240
|
if (!known.has(`${device.udid}@${device.host}`))
|
|
244
241
|
added.push(this.toIDevice(d));
|
|
245
242
|
}
|
|
246
243
|
return added;
|
|
247
244
|
});
|
|
248
245
|
}
|
|
249
|
-
|
|
246
|
+
/**
|
|
247
|
+
* A node's report of one phone (AddDevicesOptions.nodeReport). The node's
|
|
248
|
+
* `busy` goes to `nodeBusy`. Busy there makes the phone busy here; free
|
|
249
|
+
* there frees it only where this hub holds no claim, in one conditional
|
|
250
|
+
* update, so a report the node sent before the hub claimed the phone can't
|
|
251
|
+
* undo the claim.
|
|
252
|
+
*/
|
|
253
|
+
addNodeReport(device) {
|
|
250
254
|
return __awaiter(this, void 0, void 0, function* () {
|
|
255
|
+
const { udid, host } = device;
|
|
256
|
+
const nodeBusy = device.busy === true;
|
|
257
|
+
const reported = Object.assign(Object.assign({}, (0, deviceFieldOwners_1.pickNodeReportFields)(this.fromIDevice(device))), { nodeBusy });
|
|
258
|
+
const row = yield this.prisma.device.upsert({
|
|
259
|
+
where: { udid_host: { udid, host } },
|
|
260
|
+
update: nodeBusy ? Object.assign(Object.assign({}, reported), { busy: true }) : reported,
|
|
261
|
+
create: Object.assign(Object.assign({}, reported), { udid, host, busy: nodeBusy }),
|
|
262
|
+
});
|
|
263
|
+
if (nodeBusy)
|
|
264
|
+
return row;
|
|
265
|
+
// Nothing of the hub's may hold it either: a claim, or a hold such as a
|
|
266
|
+
// preview or a recording (session_id), exactly as a release checks.
|
|
267
|
+
yield this.prisma.device.updateMany({
|
|
268
|
+
where: Object.assign({ udid, host, busy: true }, deviceClaims_1.UNHELD),
|
|
269
|
+
data: { busy: false },
|
|
270
|
+
});
|
|
271
|
+
return row;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
removeDevices(filter_1) {
|
|
275
|
+
return __awaiter(this, arguments, void 0, function* (filter, options = {}) {
|
|
251
276
|
// Principal Fix: Support partial host matching (e.g., "192.168.0.100" should match "http://192.168.0.100:4723")
|
|
252
277
|
// This is necessary because the device tracker passes the raw IP, not the full URL.
|
|
278
|
+
// `exactHost` turns it off, for a caller that names rows by their own host.
|
|
253
279
|
const whereClause = {};
|
|
254
280
|
if (filter.udid) {
|
|
255
281
|
whereClause.udid = filter.udid;
|
|
256
282
|
}
|
|
257
283
|
if (filter.host) {
|
|
258
284
|
// Use Prisma's `contains` for partial matching if host doesn't look like a full URL
|
|
259
|
-
if (
|
|
285
|
+
if (options.exactHost ||
|
|
286
|
+
filter.host.startsWith('http://') ||
|
|
287
|
+
filter.host.startsWith('https://')) {
|
|
260
288
|
whereClause.host = filter.host;
|
|
261
289
|
}
|
|
262
290
|
else {
|
|
@@ -266,9 +294,9 @@ class PrismaDeviceStore {
|
|
|
266
294
|
yield this.prisma.device.deleteMany({ where: whereClause });
|
|
267
295
|
});
|
|
268
296
|
}
|
|
269
|
-
clearStorage() {
|
|
297
|
+
clearStorage(onlyHosts) {
|
|
270
298
|
return __awaiter(this, void 0, void 0, function* () {
|
|
271
|
-
yield this.prisma.device.deleteMany();
|
|
299
|
+
yield this.prisma.device.deleteMany(onlyHosts ? { where: { host: { in: [...onlyHosts] } } } : undefined);
|
|
272
300
|
});
|
|
273
301
|
}
|
|
274
302
|
findDevice(filter) {
|
|
@@ -283,8 +311,8 @@ class PrismaDeviceStore {
|
|
|
283
311
|
return devices.map((d) => this.toIDevice(d));
|
|
284
312
|
});
|
|
285
313
|
}
|
|
286
|
-
findAndLockDevice(
|
|
287
|
-
return __awaiter(this,
|
|
314
|
+
findAndLockDevice(filterOptions_1) {
|
|
315
|
+
return __awaiter(this, arguments, void 0, function* (filterOptions, options = {}) {
|
|
288
316
|
// Phase 2: exclude devices held by an active lease.
|
|
289
317
|
const activeLeases = yield this.prisma.lease.findMany({
|
|
290
318
|
where: { status: 'active' },
|
|
@@ -298,6 +326,7 @@ class PrismaDeviceStore {
|
|
|
298
326
|
if (blockedKeys.has(`${device.udid}@${device.host}`)) {
|
|
299
327
|
continue;
|
|
300
328
|
}
|
|
329
|
+
const now = Date.now();
|
|
301
330
|
const result = yield this.prisma.device.updateMany({
|
|
302
331
|
where: {
|
|
303
332
|
udid: device.udid,
|
|
@@ -305,11 +334,7 @@ class PrismaDeviceStore {
|
|
|
305
334
|
busy: false, // Double check it's still free
|
|
306
335
|
offline: false,
|
|
307
336
|
},
|
|
308
|
-
data: {
|
|
309
|
-
busy: true,
|
|
310
|
-
lastCmdExecutedAt: Date.now(),
|
|
311
|
-
sessionStartTime: Date.now(),
|
|
312
|
-
},
|
|
337
|
+
data: Object.assign({ busy: true, lastCmdExecutedAt: now, sessionStartTime: now }, (options.claim ? { claimedAt: now, claimSessionId: null } : {})),
|
|
313
338
|
});
|
|
314
339
|
if (result.count === 1) {
|
|
315
340
|
logger_1.default.info(`[PrismaStore] Successfully locked device ${device.udid} atomically`);
|
|
@@ -323,6 +348,44 @@ class PrismaDeviceStore {
|
|
|
323
348
|
return null;
|
|
324
349
|
});
|
|
325
350
|
}
|
|
351
|
+
claimForSession(udid, host, claimedAt, sessionId, update) {
|
|
352
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
353
|
+
// The pending claim this phone was allocated with, or none left at all
|
|
354
|
+
// (released while the session was created, and not taken since).
|
|
355
|
+
const ours = claimedAt == null ? [{ claimedAt: null }] : [{ claimedAt }, { claimedAt: null }];
|
|
356
|
+
const result = yield this.prisma.device.updateMany({
|
|
357
|
+
where: { udid, host, claimSessionId: null, OR: ours },
|
|
358
|
+
data: Object.assign(Object.assign({}, this.fromIDevice(update)), { busy: true, claimSessionId: sessionId, claimedAt: claimedAt !== null && claimedAt !== void 0 ? claimedAt : Date.now() }),
|
|
359
|
+
});
|
|
360
|
+
return result.count > 0;
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
releaseClaim(udid, host, ref, update) {
|
|
364
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
365
|
+
const ended = yield this.prisma.device.updateMany({
|
|
366
|
+
where: Object.assign({ udid, host }, (0, deviceClaims_1.claimWhere)(ref)),
|
|
367
|
+
data: Object.assign(Object.assign({}, this.fromIDevice(update)), deviceClaims_1.CLAIM_RESET),
|
|
368
|
+
});
|
|
369
|
+
if (ended.count === 0)
|
|
370
|
+
return false;
|
|
371
|
+
// Not `busy: false` with the step above: a node reporting the phone busy,
|
|
372
|
+
// or a hold taken since, keeps it busy.
|
|
373
|
+
yield this.prisma.device.updateMany({
|
|
374
|
+
where: Object.assign({ udid, host, busy: true }, deviceClaims_1.UNHELD),
|
|
375
|
+
data: { busy: false },
|
|
376
|
+
});
|
|
377
|
+
return true;
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
releaseLeaseLock(udid, host) {
|
|
381
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
382
|
+
const cleared = yield this.prisma.device.updateMany({
|
|
383
|
+
where: Object.assign({ udid, host, busy: true }, deviceClaims_1.UNHELD),
|
|
384
|
+
data: { busy: false },
|
|
385
|
+
});
|
|
386
|
+
return cleared.count > 0;
|
|
387
|
+
});
|
|
388
|
+
}
|
|
326
389
|
resetMetrics() {
|
|
327
390
|
return __awaiter(this, void 0, void 0, function* () {
|
|
328
391
|
yield this.prisma.device.updateMany({
|
|
@@ -77,6 +77,7 @@ const Devices_1 = __importDefault(require("./cloud/Devices"));
|
|
|
77
77
|
const DeviceLockManager_1 = require("./android/DeviceLockManager");
|
|
78
78
|
const androidIdentity_1 = require("./android/androidIdentity");
|
|
79
79
|
const deviceIdentity_1 = require("./deviceIdentity");
|
|
80
|
+
const localDeviceHosts_1 = require("./localDeviceHosts");
|
|
80
81
|
const PluginContext_1 = require("../PluginContext");
|
|
81
82
|
const typedi_2 = require("typedi");
|
|
82
83
|
let AndroidDeviceManager = class AndroidDeviceManager {
|
|
@@ -196,7 +197,11 @@ let AndroidDeviceManager = class AndroidDeviceManager {
|
|
|
196
197
|
adbInstance.adbRemoteHost === null
|
|
197
198
|
? this.pluginArgs.bindHostOrIp
|
|
198
199
|
: adbInstance.adbRemoteHost;
|
|
199
|
-
|
|
200
|
+
// This server's own row for the phone: the exact host deviceInfo
|
|
201
|
+
// would write. A node on this machine shares the IP, so its row
|
|
202
|
+
// for the same phone must not be taken for ours.
|
|
203
|
+
const ownHost = (0, localDeviceHosts_1.androidDeviceHost)(this.pluginArgs, this.hostPort, adbInstance);
|
|
204
|
+
const existingDevice = existingDeviceDetails.find((dev) => dev.udid === device.udid && dev.host === ownHost);
|
|
200
205
|
if (existingDevice) {
|
|
201
206
|
if (device.state === 'device') {
|
|
202
207
|
logger_1.default.debug(`Android Device details for ${device.udid} already available and online`);
|
|
@@ -263,16 +268,7 @@ let AndroidDeviceManager = class AndroidDeviceManager {
|
|
|
263
268
|
}
|
|
264
269
|
// Optional: a device that can't say still registers, as before.
|
|
265
270
|
const identity = yield this.getIdentity(adbInstance, device.udid, realDevice);
|
|
266
|
-
|
|
267
|
-
if (adbInstance.adbHost != null) {
|
|
268
|
-
host = `http://${adbInstance.adbHost}:${adbInstance.adbPort}`;
|
|
269
|
-
}
|
|
270
|
-
else if (pluginArgs.remoteMachineProxyIP !== undefined) {
|
|
271
|
-
host = `http://${pluginArgs.remoteMachineProxyIP}:${hostPort}`;
|
|
272
|
-
}
|
|
273
|
-
else {
|
|
274
|
-
host = `http://${pluginArgs.bindHostOrIp}:${hostPort}`;
|
|
275
|
-
}
|
|
271
|
+
const host = (0, localDeviceHosts_1.androidDeviceHost)(pluginArgs, hostPort, adbInstance);
|
|
276
272
|
return Object.assign({ adbRemoteHost: (_a = adbInstance.adbRemoteHost) !== null && _a !== void 0 ? _a : undefined, adbPort: adbInstance.adbPort, systemPort, sdk: sdk !== null && sdk !== void 0 ? sdk : 'unknown', realDevice, name: name !== null && name !== void 0 ? name : 'unknown', busy: false, state: device.state, udid: device.udid, platform: 'android', deviceType: realDevice ? 'real' : 'emulator', host, totalUtilizationTimeMilliSec: totalUtilizationTimeMilliSec, sessionStartTime: 0, userBlocked: false, ip: (0, networkAddresses_1.sanitizeDeviceNetworkIp)(yield this.getDeviceIp(adbInstance, device.udid)), cpuArchitecture: yield this.getCpuArchitecture(adbInstance, device.udid) }, identity);
|
|
277
273
|
});
|
|
278
274
|
}
|
|
@@ -477,9 +473,7 @@ let AndroidDeviceManager = class AndroidDeviceManager {
|
|
|
477
473
|
const adbRemote = pluginArgs.adbRemote;
|
|
478
474
|
if (adbRemote !== undefined && adbRemote.length > 0) {
|
|
479
475
|
const promises = adbRemote.map((value) => __awaiter(this, void 0, void 0, function* () {
|
|
480
|
-
const
|
|
481
|
-
const adbHost = adbRemoteValue[0];
|
|
482
|
-
const adbPort = parseInt(adbRemoteValue[1]) || 5037;
|
|
476
|
+
const { adbHost, adbPort } = (0, localDeviceHosts_1.parseAdbRemote)(value);
|
|
483
477
|
const cloneAdb = originalADB.clone({
|
|
484
478
|
remoteAdbHost: adbHost,
|
|
485
479
|
adbPort,
|
|
@@ -577,10 +571,14 @@ let AndroidDeviceManager = class AndroidDeviceManager {
|
|
|
577
571
|
}
|
|
578
572
|
onDeviceRemoved(device, pluginArgs) {
|
|
579
573
|
return __awaiter(this, void 0, void 0, function* () {
|
|
574
|
+
// The host onDeviceAdded files the phone under (deviceInfo with the local
|
|
575
|
+
// adb). Exact: a bare IP is matched as a substring by the store and by
|
|
576
|
+
// the hub, and would also take another server's row with this udid.
|
|
580
577
|
const clonedDevice = {
|
|
581
578
|
udid: device['id'],
|
|
582
|
-
host: pluginArgs.
|
|
579
|
+
host: (0, localDeviceHosts_1.androidDeviceHost)(pluginArgs, this.hostPort),
|
|
583
580
|
state: device.type,
|
|
581
|
+
nodeId: this.nodeId,
|
|
584
582
|
};
|
|
585
583
|
if (pluginArgs.hub != undefined) {
|
|
586
584
|
const nodeDevices = new NodeDevices_1.default(pluginArgs.hub, {
|
|
@@ -63,6 +63,9 @@ const schedule = __importStar(require("node-schedule"));
|
|
|
63
63
|
const web_config_service_1 = require("../data-service/web-config-service");
|
|
64
64
|
const SessionManager_1 = require("../sessions/SessionManager");
|
|
65
65
|
const EventBus_1 = require("../services/EventBus");
|
|
66
|
+
const deviceClaims_1 = require("../data-service/deviceClaims");
|
|
67
|
+
const PluginContext_1 = require("../PluginContext");
|
|
68
|
+
const localDeviceHosts_1 = require("./localDeviceHosts");
|
|
66
69
|
let HealthMonitorService = class HealthMonitorService {
|
|
67
70
|
constructor(webConfigService) {
|
|
68
71
|
this.webConfigService = webConfigService;
|
|
@@ -145,14 +148,26 @@ let HealthMonitorService = class HealthMonitorService {
|
|
|
145
148
|
const devices = yield store.getAllDevices();
|
|
146
149
|
const manager = typedi_1.Container.get(index_1.XenonManager);
|
|
147
150
|
const instances = yield manager.deviceInstances();
|
|
151
|
+
const context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
152
|
+
const local = (0, localDeviceHosts_1.localDeviceHosts)(context.pluginArgs, context.port);
|
|
148
153
|
for (const device of devices) {
|
|
149
|
-
// Only
|
|
150
|
-
|
|
154
|
+
// Only this server's own phones. A hub keeps its nodes' phones in the
|
|
155
|
+
// same table; its adb can't reach them, so each came back unhealthy,
|
|
156
|
+
// was written over the node's report, "recovered" and, when busy with
|
|
157
|
+
// a session not in this hub's memory, reclaimed. Their node checks them.
|
|
158
|
+
if (device.cloud || !(0, localDeviceHosts_1.isOwnDevice)(local, context.nodeId, device))
|
|
151
159
|
continue;
|
|
152
160
|
// Principal Intelligence: Health monitor must detect "Zombie Busy" devices.
|
|
153
161
|
// A device is a Zombie if it's marked busy in DB but NOT found in Hub's session map
|
|
154
162
|
// AND not currently streaming to a manual viewer.
|
|
155
163
|
if (device.busy) {
|
|
164
|
+
// Not a zombie: a session still being created on it (its session is
|
|
165
|
+
// in no map yet; the claim has its own timeout), or, on a hub, a
|
|
166
|
+
// node reporting it busy there (deviceClaims.ts).
|
|
167
|
+
if (((0, deviceClaims_1.isPendingClaim)(device) && !(0, deviceClaims_1.pendingClaimExpired)(device, Date.now())) ||
|
|
168
|
+
device.nodeBusy) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
156
171
|
const hasActiveSession = SessionManager_1.SESSION_MANAGER.isValidSession(device.session_id || '');
|
|
157
172
|
const isManualStream = (_a = device.session_id) === null || _a === void 0 ? void 0 : _a.startsWith('manual_');
|
|
158
173
|
// NEW: Check for active manual streams (dashboard viewers)
|
|
@@ -99,7 +99,11 @@ let IOSDeviceManager = class IOSDeviceManager {
|
|
|
99
99
|
result.screenHeight = String(streamStatus.screenHeight);
|
|
100
100
|
}
|
|
101
101
|
else {
|
|
102
|
-
|
|
102
|
+
// This phone's own row: another server's for the same udid is not it.
|
|
103
|
+
const storeDevice = yield device_store_1.DeviceStoreFactory.getStore().findDevice({
|
|
104
|
+
udid: device.udid,
|
|
105
|
+
host: device.host,
|
|
106
|
+
});
|
|
103
107
|
if (storeDevice === null || storeDevice === void 0 ? void 0 : storeDevice.screenWidth) {
|
|
104
108
|
result.screenWidth = storeDevice.screenWidth;
|
|
105
109
|
result.screenHeight = storeDevice.screenHeight;
|