@xenon-device-management/xenon 2.13.2 → 2.15.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 +17 -8
- package/lib/package.json +5 -5
- package/lib/public/assets/{AnnotationOverlay-BXruClDk.js → AnnotationOverlay-Btq3dZRI.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-qTS4LPLC.js → ApiKeyGate-C9OoAgg_.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-DBC_xczB.js +2 -0
- package/lib/public/assets/{EmptyState-Cr17PdDv.js → EmptyState-DqWU2-vM.js} +1 -1
- package/lib/public/assets/{FieldGroup-Bvn08svv.js → FieldGroup-CveQ3xrA.js} +1 -1
- package/lib/public/assets/{FilterMenu-DDSO8aDf.js → FilterMenu-BfKDSfHz.js} +1 -1
- package/lib/public/assets/{Menu-B-Qn2X3T.js → Menu-CtdCybzA.js} +1 -1
- package/lib/public/assets/{Modal-Cg9V5JYU.js → Modal-Bsvj1tm2.js} +1 -1
- package/lib/public/assets/{RecordingPage-BTUs9VTW.js → RecordingPage-CRofOiyx.js} +1 -1
- package/lib/public/assets/{RecordingsPage-DYcfxITK.js → RecordingsPage-jNQHb2IR.js} +2 -2
- package/lib/public/assets/{SegmentedControl-Zy7suhT5.js → SegmentedControl-C8HtTha-.js} +1 -1
- package/lib/public/assets/{SettingCard-CWYmycoe.js → SettingCard-D8K1LZNo.js} +1 -1
- package/lib/public/assets/{Table-io9fwa98.js → Table-zNTX92Pn.js} +1 -1
- package/lib/public/assets/{activity-Bdf_uIWl.js → activity-CJ8LwZq9.js} +1 -1
- package/lib/public/assets/ai-settings-CgxQZqrc.js +16 -0
- package/lib/public/assets/{api-keys-CPqtVpm3.js → api-keys-ECpo-zTo.js} +1 -1
- package/lib/public/assets/{apps-BgZan0H9.js → apps-BjGymyf3.js} +2 -2
- package/lib/public/assets/{arrow-left-B27ckB31.js → arrow-left-BWfCbeHP.js} +1 -1
- package/lib/public/assets/{arrow-right-DOF8ITCz.js → arrow-right-DC7UGf_n.js} +1 -1
- package/lib/public/assets/{arrow-up-right-eFG5KTFS.js → arrow-up-right-BOjwybDk.js} +1 -1
- package/lib/public/assets/{auth-shell-DlC3_27j.js → auth-shell-CxMTo9aH.js} +2 -2
- package/lib/public/assets/{builds-page-BuXpPfKo.js → builds-page-DOCm4HkA.js} +1 -1
- package/lib/public/assets/{button-fIW8owMI.js → button-Cg08-EMT.js} +1 -1
- package/lib/public/assets/{calendar-WNjmrF-a.js → calendar-CA1ikLJs.js} +1 -1
- package/lib/public/assets/{check-BdTh1Sq7.js → check-BMTaH72W.js} +1 -1
- package/lib/public/assets/{chevron-right-B4BKlsqk.js → chevron-right-B2QOv9Nx.js} +1 -1
- package/lib/public/assets/{circle-check-zJU80_EF.js → circle-check-CYXBoUqT.js} +1 -1
- package/lib/public/assets/{circle-x-5Uwquo0h.js → circle-x-DTwnSAnl.js} +1 -1
- package/lib/public/assets/{clock-BtUprDsZ.js → clock-C8_HViQP.js} +1 -1
- package/lib/public/assets/{copy-DGSbZI3u.js → copy-Be7LNMXV.js} +1 -1
- package/lib/public/assets/derive-UnLRp4r3.js +1 -0
- package/lib/public/assets/device-explorer-BOqtfGXz.js +282 -0
- package/lib/public/assets/{download-WG6l7BgD.js → download-B56lwxWP.js} +1 -1
- package/lib/public/assets/forgot-password-CCU6bzLn.js +1 -0
- package/lib/public/assets/h264Stream-C-OtmIuk.js +26 -0
- package/lib/public/assets/index-B9KQ6LsY.js +15 -0
- package/lib/public/assets/{index-DI1fGIyj.js → index-BOxe43p1.js} +5 -5
- package/lib/public/assets/{index-HDQVe1gi.js → index-CfxsUslp.js} +1 -1
- package/lib/public/assets/{input-BjDRKCea.js → input-CZHFjbSg.js} +1 -1
- package/lib/public/assets/{line-chart-C0zdgd0v.js → line-chart-Gl_Dz3X_.js} +1 -1
- package/lib/public/assets/{list-checks-BdW5CjM8.js → list-checks-CCgjeyGY.js} +1 -1
- package/lib/public/assets/{lock-Dy3L4I8E.js → lock-CkOwgDcx.js} +1 -1
- package/lib/public/assets/{login-BHbY9CLG.js → login-D5cc4w7M.js} +1 -1
- package/lib/public/assets/maintenance-settings-t7qr3fvX.js +6 -0
- package/lib/public/assets/{monitor-8xjkeHHi.js → monitor-DEJixKQO.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CXdKMXQ8.js → mouse-pointer-2-CtOrk4M8.js} +1 -1
- package/lib/public/assets/{network-Cj8JOKON.js → network-Dii9qdvk.js} +1 -1
- package/lib/public/assets/{overview-CTg7F0im.js → overview-rolAACrO.js} +1 -1
- package/lib/public/assets/{page-header--aTyJXq6.js → page-header-COQZxJfR.js} +1 -1
- package/lib/public/assets/{play-B-FodgHV.js → play-DrN1Se_R.js} +1 -1
- package/lib/public/assets/{plus-CsIk10Cq.js → plus-5aq-9uRx.js} +1 -1
- package/lib/public/assets/{profile-page-DkFKkMfl.js → profile-page-6i5kY6kF.js} +1 -1
- package/lib/public/assets/{reset-password-I2-zqLlI.js → reset-password-sp2IBrc7.js} +1 -1
- package/lib/public/assets/runbook-page-DJDjeopU.js +253 -0
- package/lib/public/assets/{select-DvCSv25z.js → select-Bpyn7rWO.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BLVfMJs_.js → selector-detail-redirect-BH74vmKv.js} +1 -1
- package/lib/public/assets/{selector-health-page-oqadRokn.js → selector-health-page-Cw_g218H.js} +2 -2
- package/lib/public/assets/session-detail-page-AZJE6XXE.js +4 -0
- package/lib/public/assets/settings-C3uyGhyc.js +6 -0
- package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
- package/lib/public/assets/{stat-tile-DV9Ix-4O.js → stat-tile-xVqAKIlI.js} +1 -1
- package/lib/public/assets/{tablet-6b_SEN2X.js → tablet-BxUScAeO.js} +1 -1
- package/lib/public/assets/{teams-BjZyfuHh.js → teams-B5ljGkY2.js} +1 -1
- package/lib/public/assets/{trash-2-CTZpPSdd.js → trash-2-B701i8sy.js} +1 -1
- package/lib/public/assets/{upload-P3XzwQUb.js → upload-C2gr44RS.js} +1 -1
- package/lib/public/assets/{use-builds-data-1cOCMf-z.js → use-builds-data-NeQOFEoy.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DgF9w088.js → use-password-reset-mode-DEzJq5ef.js} +1 -1
- package/lib/public/assets/{users-DMF9Mk5H.js → users-D_vv_yz4.js} +1 -1
- package/lib/public/assets/{users-CiNTKUiz.js → users-MY6eNe6s.js} +1 -1
- package/lib/public/assets/{video-off-CHp1KU_T.js → video-off-OK1lNZQn.js} +1 -1
- package/lib/public/assets/webhook-settings-BU-C9lBf.js +1 -0
- package/lib/public/assets/{zap-DRz0tDq5.js → zap-DO9ry1iz.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +23 -25
- package/lib/src/XenonCapabilityManager.js +22 -5
- package/lib/src/app/dashboardPluginLink.js +117 -0
- package/lib/src/app/index.js +13 -34
- package/lib/src/app/openapi/control.yaml +73 -20
- package/lib/src/app/openapi/grid.yaml +31 -7
- package/lib/src/app/openapi/identity.yaml +103 -31
- package/lib/src/app/openapi/platform.yaml +165 -39
- package/lib/src/app/openapi/sessions.yaml +10 -3
- package/lib/src/app/routers/apikeys.js +14 -2
- package/lib/src/app/routers/auth.js +29 -5
- package/lib/src/app/routers/control.js +20 -2
- package/lib/src/app/routers/dashboard.js +69 -26
- package/lib/src/app/routers/grid.js +37 -13
- package/lib/src/app/routers/profile.js +21 -0
- package/lib/src/app/routers/streamType.js +7 -2
- package/lib/src/app/routers/users.js +10 -5
- package/lib/src/app/routers/webhook.js +8 -4
- package/lib/src/app/swagger.js +2 -0
- package/lib/src/app/ws/h264StreamWs.js +10 -1
- package/lib/src/config.js +51 -5
- package/lib/src/dashboard/commands.js +185 -141
- package/lib/src/dashboard/event-manager.js +88 -67
- package/lib/src/dashboard/services/failure-analysis-service.js +136 -45
- package/lib/src/dashboard/sessionCommandSummary.js +39 -0
- package/lib/src/data-service/appleFamily.js +43 -0
- package/lib/src/data-service/db.js +11 -0
- package/lib/src/data-service/device-service.js +6 -1
- package/lib/src/data-service/device-store.js +76 -3
- package/lib/src/data-service/deviceClaims.js +37 -0
- package/lib/src/data-service/deviceFieldOwners.js +25 -11
- package/lib/src/data-service/deviceSettings.js +79 -0
- package/lib/src/data-service/prisma-store.js +169 -15
- package/lib/src/data-service/web-config-service.js +50 -92
- package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
- package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
- package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
- package/lib/src/device-managers/ios/WDAClient.js +44 -43
- package/lib/src/device-managers/shellCommands.js +96 -0
- package/lib/src/device-utils.js +114 -40
- package/lib/src/gateway/defaultGateway.js +1 -1
- package/lib/src/gateway/forwardToNode.js +2 -0
- package/lib/src/gateway/healReport.js +87 -0
- package/lib/src/gateway/sessionGateway.js +12 -2
- package/lib/src/generated/client/edge.js +18 -3
- package/lib/src/generated/client/index-browser.js +15 -0
- package/lib/src/generated/client/index.d.ts +2086 -379
- package/lib/src/generated/client/index.js +18 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +38 -7
- package/lib/src/generated/client/wasm.js +15 -0
- package/lib/src/interceptors/CommandInterceptor.js +271 -188
- package/lib/src/interceptors/xenonScripts.js +55 -0
- package/lib/src/interfaces/IPluginArgs.js +1 -2
- package/lib/src/middleware/authMiddleware.js +15 -3
- package/lib/src/middleware/commandCaller.js +3 -3
- package/lib/src/middleware/verifyCredential.js +58 -1
- package/lib/src/plugin.js +23 -4
- package/lib/src/prisma.js +1 -1
- package/lib/src/scripts/database-check.js +124 -0
- package/lib/src/services/AICommandService.js +9 -2
- package/lib/src/services/AIService.js +281 -32
- package/lib/src/services/CleanupService.js +6 -1
- package/lib/src/services/EmailService.js +29 -0
- package/lib/src/services/InterceptorService.js +70 -13
- package/lib/src/services/NetworkConditioningService.js +85 -88
- package/lib/src/services/NotificationService.js +36 -29
- package/lib/src/services/OrphanSweeper.js +7 -0
- package/lib/src/services/PortAllocator.js +5 -0
- package/lib/src/services/SelectorVerificationJob.js +15 -4
- package/lib/src/services/ServerManager.js +105 -9
- package/lib/src/services/SessionLifecycleService.js +133 -75
- package/lib/src/services/ShutdownCoordinator.js +33 -0
- package/lib/src/services/SocketServer.js +19 -3
- package/lib/src/services/TeamService.js +20 -3
- package/lib/src/services/bug-report/video-slice.js +4 -1
- package/lib/src/services/device-access/leaseOverride.js +7 -5
- package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
- package/lib/src/services/healing/HealEtalonService.js +3 -2
- package/lib/src/services/healing/HealingOrchestrator.js +85 -71
- package/lib/src/services/healing/OcrHealingProvider.js +41 -64
- package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
- package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
- package/lib/src/services/healing/fingerprintIdentity.js +126 -0
- package/lib/src/services/healing/resilioPath.js +274 -0
- package/lib/src/services/interceptor/AndroidProxyAdapter.js +18 -0
- package/lib/src/services/interceptor/interceptorOptions.js +44 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +271 -0
- package/lib/src/services/logcat/deviceLogBook.js +163 -0
- package/lib/src/services/network/AndroidRadios.js +71 -0
- package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
- package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
- package/lib/src/services/network/adbForPhone.js +60 -0
- package/lib/src/services/network/phoneNetworkLock.js +20 -0
- package/lib/src/services/network/xenonProxy.js +107 -0
- package/lib/src/services/ocr/ocrData.js +118 -0
- package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
- package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
- package/lib/src/services/omni-vision/OmniVisionService.js +132 -95
- package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
- package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
- package/lib/src/services/omni-vision/screenScale.js +68 -0
- package/lib/src/services/passwordResetLink.js +50 -9
- package/lib/src/services/passwordResetMode.js +3 -1
- package/lib/src/services/publicUrl.js +45 -0
- package/lib/src/services/recording/concurrency-gate.js +12 -3
- package/lib/src/services/session/sessionIdentity.js +5 -3
- package/lib/src/services/session/shutdownReason.js +26 -0
- package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
- package/lib/src/services/settings/aiEngineSettings.js +219 -0
- package/lib/src/services/settings/labSettings.js +149 -0
- package/lib/src/services/token/mintedLifetime.js +51 -0
- package/lib/src/services/webhookEvents.js +173 -0
- package/lib/src/sessions/SessionManager.js +8 -2
- package/lib/src/sessions/sessionMemory.js +25 -0
- package/lib/test/helpers/fake-adb.js +58 -0
- package/lib/test/helpers/fake-ai-provider.js +188 -0
- package/lib/test/helpers/fake-device-table.js +63 -24
- package/lib/test/helpers/loki-stores.js +33 -0
- package/lib/test/helpers/page-sources.js +164 -0
- package/lib/test/helpers/scratch-database.js +58 -13
- package/lib/test/integration/audit-ingest.spec.js +10 -10
- package/lib/test/integration/auth-flow.spec.js +9 -14
- package/lib/test/integration/auth-rate-limit.spec.js +6 -3
- package/lib/test/integration/bug-report-route.spec.js +22 -2
- package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
- package/lib/test/integration/forgot-password.spec.js +17 -18
- package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
- package/lib/test/integration/migration.spec.js +6 -1
- package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
- package/lib/test/integration/ports-router.spec.js +4 -4
- package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
- package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
- package/lib/test/integration/profile-tokens.spec.js +10 -16
- package/lib/test/integration/recordings-router.spec.js +16 -16
- package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
- package/lib/test/integration/role-matrix.spec.js +4 -9
- package/lib/test/integration/sdk-leases-router.spec.js +9 -9
- package/lib/test/integration/sdk-version.spec.js +2 -2
- package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
- package/lib/test/integration/team-membership-migration.spec.js +73 -12
- package/lib/test/integration/team-visibility-apps.spec.js +15 -22
- package/lib/test/integration/team-visibility-control.spec.js +96 -69
- package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
- package/lib/test/integration/team-visibility-grid.spec.js +16 -15
- package/lib/test/integration/team-visibility-leases.spec.js +8 -11
- package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
- package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
- package/lib/test/integration/users-flow.spec.js +7 -12
- package/lib/test/integration/users-lockout.spec.js +5 -3
- package/lib/test/unit/EmailService.test.js +66 -0
- package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
- package/lib/test/unit/TeamService.test.js +3 -0
- package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
- package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
- package/lib/test/unit/ai-engine-settings.spec.js +338 -0
- package/lib/test/unit/ai-provider-switch.spec.js +130 -0
- package/lib/test/unit/ai-rate-limit.spec.js +126 -0
- package/lib/test/unit/android-h264-viewers.spec.js +272 -0
- package/lib/test/unit/android-preview-hold.spec.js +3 -2
- package/lib/test/unit/apikeys-router.spec.js +84 -0
- package/lib/test/unit/apple-family.spec.js +36 -0
- package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
- package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
- package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
- package/lib/test/unit/cleanup-settings-db.spec.js +103 -0
- package/lib/test/unit/cleanup-settings.spec.js +244 -0
- package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
- package/lib/test/unit/dashboard-commands.spec.js +249 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +207 -0
- package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
- package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
- package/lib/test/unit/database-check.spec.js +108 -0
- package/lib/test/unit/default-plugin-args.spec.js +33 -0
- package/lib/test/unit/device-log-book.spec.js +180 -0
- package/lib/test/unit/device-offline-webhook.spec.js +81 -0
- package/lib/test/unit/device-setting-columns.spec.js +66 -0
- package/lib/test/unit/device-settings-loki.spec.js +87 -0
- package/lib/test/unit/device-settings-survive.spec.js +555 -0
- package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
- package/lib/test/unit/emulator-boot.spec.js +91 -0
- package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
- package/lib/test/unit/event-log-session-data.spec.js +244 -0
- package/lib/test/unit/failure-analysis.spec.js +182 -0
- package/lib/test/unit/failure-categories.spec.js +128 -0
- package/lib/test/unit/fingerprint-identity.spec.js +433 -0
- package/lib/test/unit/h264-multiplexer.spec.js +46 -0
- package/lib/test/unit/heal-report.spec.js +71 -0
- package/lib/test/unit/healing-instrumentation.spec.js +6 -1
- package/lib/test/unit/healing-tier-gate.spec.js +73 -29
- package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +219 -0
- package/lib/test/unit/hub-node-same-host.spec.js +5 -0
- package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
- package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
- package/lib/test/unit/json-logging-option.spec.js +74 -0
- package/lib/test/unit/lab-settings.spec.js +218 -0
- package/lib/test/unit/lease/lease-override.spec.js +23 -6
- package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
- package/lib/test/unit/max-sessions.spec.js +240 -0
- package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
- package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
- package/lib/test/unit/ocr-offline.spec.js +213 -0
- package/lib/test/unit/ocr-text-match.spec.js +44 -0
- package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
- package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
- package/lib/test/unit/omni-vision-ios-points.spec.js +159 -0
- package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
- package/lib/test/unit/omni-vision.spec.js +6 -8
- package/lib/test/unit/package-engines.spec.js +27 -0
- package/lib/test/unit/partial-config-file.spec.js +14 -0
- package/lib/test/unit/passwordResetMode.test.js +10 -2
- package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
- package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
- package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
- package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
- package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
- package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
- package/lib/test/unit/phone-network/session-start.spec.js +110 -0
- package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
- package/lib/test/unit/plugin.spec.js +3 -3
- package/lib/test/unit/profile-router.test.js +122 -1
- package/lib/test/unit/queue-requester.spec.js +1 -1
- package/lib/test/unit/recording-options.spec.js +181 -0
- package/lib/test/unit/reset-link-address.spec.js +266 -0
- package/lib/test/unit/reset-link.test.js +10 -4
- package/lib/test/unit/resilio-path-healing.spec.js +270 -0
- package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
- package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
- package/lib/test/unit/selector-verification-job.spec.js +13 -13
- package/lib/test/unit/self-healing-boot.spec.js +80 -0
- package/lib/test/unit/self-healing-setting.spec.js +273 -0
- package/lib/test/unit/session-app-resolution.spec.js +5 -1
- package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
- package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
- package/lib/test/unit/session-credential-strip.spec.js +1 -1
- package/lib/test/unit/session-device-logs.spec.js +347 -0
- package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
- package/lib/test/unit/session-failed-webhook.spec.js +170 -0
- package/lib/test/unit/session-log-indexes.spec.js +64 -3
- package/lib/test/unit/shell-commands.spec.js +133 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
- package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
- package/lib/test/unit/stream-start-pipelines.spec.js +264 -0
- package/lib/test/unit/stream-status-type.spec.js +4 -0
- package/lib/test/unit/token-mint-limits.spec.js +165 -0
- package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
- package/lib/test/unit/verify-credential.spec.js +33 -0
- package/lib/test/unit/vision-assertion.spec.js +119 -0
- package/lib/test/unit/wda-client-shell.spec.js +133 -0
- package/lib/test/unit/web-config-service.spec.js +98 -0
- package/lib/test/unit/webhook-delivery.spec.js +24 -0
- package/lib/test/unit/webhook-payloads.spec.js +273 -0
- package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
- package/package.json +5 -5
- package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
- package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
- package/prisma/schema.prisma +38 -7
- package/schema.json +23 -25
- package/scripts/generate-types-from-schema.js +1 -2
- package/scripts/lib/always-present-plugin-args.js +4 -1
- package/lib/public/assets/BugReportButton-BoGhlI7F.js +0 -15
- package/lib/public/assets/DeviceMosaicView-8J0oYqMM.js +0 -2
- package/lib/public/assets/ai-settings-Bce5SJob.js +0 -21
- package/lib/public/assets/device-explorer-C-KnhMHI.js +0 -279
- package/lib/public/assets/forgot-password-DnudqFFo.js +0 -1
- package/lib/public/assets/maintenance-settings-zDVlOMVK.js +0 -6
- package/lib/public/assets/recording-group-store-WVM3hoXn.js +0 -26
- package/lib/public/assets/runbook-page-DJTVCJX7.js +0 -124
- package/lib/public/assets/session-detail-page-CGn55Pvl.js +0 -4
- package/lib/public/assets/settings-MsVU2ukE.js +0 -6
- package/lib/public/assets/webhook-settings-DiS8geIm.js +0 -1
- /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
|
@@ -1,43 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
2
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
19
3
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
20
4
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
21
5
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
22
6
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
23
7
|
};
|
|
24
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
-
var ownKeys = function(o) {
|
|
26
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
-
var ar = [];
|
|
28
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
-
return ar;
|
|
30
|
-
};
|
|
31
|
-
return ownKeys(o);
|
|
32
|
-
};
|
|
33
|
-
return function (mod) {
|
|
34
|
-
if (mod && mod.__esModule) return mod;
|
|
35
|
-
var result = {};
|
|
36
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
-
__setModuleDefault(result, mod);
|
|
38
|
-
return result;
|
|
39
|
-
};
|
|
40
|
-
})();
|
|
41
8
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
42
9
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
43
10
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -54,6 +21,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
54
21
|
exports.NetworkConditioningService = void 0;
|
|
55
22
|
const typedi_1 = require("typedi");
|
|
56
23
|
const logger_1 = __importDefault(require("../logger"));
|
|
24
|
+
const adbForPhone_1 = require("./network/adbForPhone");
|
|
25
|
+
const AndroidRadios_1 = require("./network/AndroidRadios");
|
|
26
|
+
const PhoneNetworkLedger_1 = require("./network/PhoneNetworkLedger");
|
|
27
|
+
const phoneNetworkLock_1 = require("./network/phoneNetworkLock");
|
|
57
28
|
const NETWORK_PROFILES = {
|
|
58
29
|
Normal: null,
|
|
59
30
|
'4G': { latencyMs: 20, downloadKbps: 15000, uploadKbps: 7500 },
|
|
@@ -62,26 +33,47 @@ const NETWORK_PROFILES = {
|
|
|
62
33
|
Offline: { latencyMs: 0, downloadKbps: 0, uploadKbps: 0 },
|
|
63
34
|
};
|
|
64
35
|
/**
|
|
65
|
-
*
|
|
36
|
+
* A session's network profile (`xe:network_profile`).
|
|
37
|
+
*
|
|
38
|
+
* Every profile's delay is added to each of the session's commands by the
|
|
39
|
+
* session gateway (getLatency). On an Android phone `Offline` also turns
|
|
40
|
+
* mobile data and Wi-Fi off, and `Normal` turns them on; 4G, 3G and Edge
|
|
41
|
+
* change nothing on the phone. An iPhone, real or simulated, gets the delay
|
|
42
|
+
* only: Xcode has no command to change a simulator's network (`xcrun simctl`
|
|
43
|
+
* has no `network` subcommand in Xcode 26), and a real iPhone's can't be
|
|
44
|
+
* changed from a Mac.
|
|
45
|
+
*
|
|
46
|
+
* What `Offline` changed is undone when the session ends, however it ends
|
|
47
|
+
* (PhoneNetworkRestore), and written down meanwhile (PhoneNetworkLedger) so a
|
|
48
|
+
* restart can undo it after a crash.
|
|
66
49
|
*/
|
|
67
50
|
let NetworkConditioningService = class NetworkConditioningService {
|
|
68
51
|
constructor() {
|
|
69
52
|
this.log = logger_1.default.scope('NetworkConditioning');
|
|
70
53
|
this.activeConditions = new Map();
|
|
54
|
+
this.radios = new AndroidRadios_1.AndroidRadios(adbForPhone_1.adbForPhone);
|
|
55
|
+
this.toldAboutIPhones = false;
|
|
71
56
|
}
|
|
72
57
|
/**
|
|
73
58
|
* Applies a network profile to a session/device
|
|
74
59
|
*/
|
|
75
60
|
applyProfile(sessionId, device, profile) {
|
|
76
61
|
return __awaiter(this, void 0, void 0, function* () {
|
|
62
|
+
var _a;
|
|
77
63
|
this.log.info(`Applying network profile '${profile}' to session ${sessionId} on device ${device.udid}`);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (device.platform === 'android') {
|
|
81
|
-
|
|
64
|
+
const condition = { profile, device };
|
|
65
|
+
this.activeConditions.set(sessionId, condition);
|
|
66
|
+
if (((_a = device.platform) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'android') {
|
|
67
|
+
if (profile === 'Offline')
|
|
68
|
+
yield this.takeOffline(sessionId, condition);
|
|
69
|
+
else if (profile === 'Normal')
|
|
70
|
+
yield this.turnOnline(device.udid);
|
|
71
|
+
// 4G, 3G and Edge: the delay only.
|
|
82
72
|
}
|
|
83
|
-
else if (
|
|
84
|
-
|
|
73
|
+
else if (!this.toldAboutIPhones) {
|
|
74
|
+
this.toldAboutIPhones = true;
|
|
75
|
+
this.log.info('Network profiles on iPhones and iOS simulators only delay each command: the ' +
|
|
76
|
+
"device's own network isn't changed, so Offline leaves it online.");
|
|
85
77
|
}
|
|
86
78
|
});
|
|
87
79
|
}
|
|
@@ -89,70 +81,75 @@ let NetworkConditioningService = class NetworkConditioningService {
|
|
|
89
81
|
* Gets the active profile for a session
|
|
90
82
|
*/
|
|
91
83
|
getProfile(sessionId) {
|
|
92
|
-
|
|
84
|
+
var _a;
|
|
85
|
+
return (_a = this.activeConditions.get(sessionId)) === null || _a === void 0 ? void 0 : _a.profile;
|
|
86
|
+
}
|
|
87
|
+
/** Whether the session has a profile on this server. */
|
|
88
|
+
has(sessionId) {
|
|
89
|
+
return this.activeConditions.has(sessionId);
|
|
90
|
+
}
|
|
91
|
+
/** The sessions with a profile on this server. */
|
|
92
|
+
sessionIds() {
|
|
93
|
+
return [...this.activeConditions.keys()];
|
|
93
94
|
}
|
|
94
95
|
/**
|
|
95
|
-
*
|
|
96
|
+
* Ends the session's profile and undoes what it changed on the phone, once:
|
|
97
|
+
* the entry is taken before anything is awaited, so a second call finds
|
|
98
|
+
* nothing to do. A phone that can't be reached keeps its record in the
|
|
99
|
+
* ledger, and is put back at its next session or this server's next start.
|
|
96
100
|
*/
|
|
97
|
-
reset(sessionId
|
|
101
|
+
reset(sessionId) {
|
|
98
102
|
return __awaiter(this, void 0, void 0, function* () {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
const condition = this.activeConditions.get(sessionId);
|
|
104
|
+
if (!condition)
|
|
105
|
+
return;
|
|
106
|
+
this.activeConditions.delete(sessionId);
|
|
107
|
+
this.log.info(`Resetting network conditions for session ${sessionId}`);
|
|
108
|
+
const prior = condition.offlinePrior;
|
|
109
|
+
if (!prior)
|
|
110
|
+
return;
|
|
111
|
+
const udid = condition.device.udid;
|
|
112
|
+
yield (0, phoneNetworkLock_1.withPhoneNetworkLock)(udid, () => __awaiter(this, void 0, void 0, function* () {
|
|
113
|
+
try {
|
|
114
|
+
yield this.radios.turnOn(udid, AndroidRadios_1.AndroidRadios.toRestore(prior));
|
|
115
|
+
yield typedi_1.Container.get(PhoneNetworkLedger_1.PhoneNetworkLedger).settle(sessionId, 'offline');
|
|
116
|
+
this.log.info(`📱 Android: network put back on ${udid} after session ${sessionId}`);
|
|
104
117
|
}
|
|
105
|
-
|
|
106
|
-
|
|
118
|
+
catch (e) {
|
|
119
|
+
this.log.warn(`⚠️ Could not put the network back on ${udid} after session ${sessionId}: ${e === null || e === void 0 ? void 0 : e.message}. ` +
|
|
120
|
+
"Xenon tries again at the phone's next session and when it restarts.");
|
|
107
121
|
}
|
|
108
|
-
}
|
|
122
|
+
}));
|
|
109
123
|
});
|
|
110
124
|
}
|
|
111
|
-
|
|
125
|
+
takeOffline(sessionId, condition) {
|
|
112
126
|
return __awaiter(this, void 0, void 0, function* () {
|
|
127
|
+
const udid = condition.device.udid;
|
|
113
128
|
try {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
yield
|
|
121
|
-
}
|
|
122
|
-
else if (profile === 'Normal') {
|
|
123
|
-
this.log.info(`📱 Android: Enabling data for ${udid}`);
|
|
124
|
-
yield execAsync(`adb -s ${udid} shell svc data enable`);
|
|
125
|
-
yield execAsync(`adb -s ${udid} shell svc wifi enable`);
|
|
126
|
-
}
|
|
127
|
-
// Latency is handled by the proxy
|
|
129
|
+
yield (0, phoneNetworkLock_1.withPhoneNetworkLock)(udid, () => __awaiter(this, void 0, void 0, function* () {
|
|
130
|
+
const prior = yield this.radios.read(udid);
|
|
131
|
+
// Written down before the change: a crash after it must still find it.
|
|
132
|
+
yield typedi_1.Container.get(PhoneNetworkLedger_1.PhoneNetworkLedger).note(sessionId, udid, { offline: prior });
|
|
133
|
+
condition.offlinePrior = prior;
|
|
134
|
+
this.log.info(`📱 Android: turning mobile data and Wi-Fi off on ${udid}`);
|
|
135
|
+
yield this.radios.turnOff(udid);
|
|
136
|
+
}));
|
|
128
137
|
}
|
|
129
138
|
catch (e) {
|
|
130
|
-
this.log.warn(`⚠️ Failed to apply Android conditioning for ${udid}: ${e.message}`);
|
|
139
|
+
this.log.warn(`⚠️ Failed to apply Android conditioning for ${udid}: ${e === null || e === void 0 ? void 0 : e.message}`);
|
|
131
140
|
}
|
|
132
141
|
});
|
|
133
142
|
}
|
|
134
|
-
|
|
143
|
+
turnOnline(udid) {
|
|
135
144
|
return __awaiter(this, void 0, void 0, function* () {
|
|
136
145
|
try {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// Profiles: wifi, 3g, edge, etc. (depends on OS version)
|
|
142
|
-
let status = 'none';
|
|
143
|
-
if (profile === '3G')
|
|
144
|
-
status = '3g';
|
|
145
|
-
if (profile === 'Edge')
|
|
146
|
-
status = 'edge';
|
|
147
|
-
if (profile === 'Offline')
|
|
148
|
-
status = 'off';
|
|
149
|
-
if (profile === 'Normal')
|
|
150
|
-
status = 'none';
|
|
151
|
-
this.log.info(`🍎 iOS Sim: Setting network to ${status} for ${udid}`);
|
|
152
|
-
yield execAsync(`xcrun simctl network ${udid} status ${status}`);
|
|
146
|
+
yield (0, phoneNetworkLock_1.withPhoneNetworkLock)(udid, () => __awaiter(this, void 0, void 0, function* () {
|
|
147
|
+
this.log.info(`📱 Android: turning mobile data and Wi-Fi on on ${udid}`);
|
|
148
|
+
yield this.radios.turnOn(udid, { data: true, wifi: true });
|
|
149
|
+
}));
|
|
153
150
|
}
|
|
154
151
|
catch (e) {
|
|
155
|
-
this.log.warn(`⚠️ Failed to apply
|
|
152
|
+
this.log.warn(`⚠️ Failed to apply Android conditioning for ${udid}: ${e === null || e === void 0 ? void 0 : e.message}`);
|
|
156
153
|
}
|
|
157
154
|
});
|
|
158
155
|
}
|
|
@@ -161,10 +158,10 @@ let NetworkConditioningService = class NetworkConditioningService {
|
|
|
161
158
|
*/
|
|
162
159
|
getLatency(sessionId) {
|
|
163
160
|
var _a;
|
|
164
|
-
const
|
|
165
|
-
if (!
|
|
161
|
+
const condition = this.activeConditions.get(sessionId);
|
|
162
|
+
if (!condition)
|
|
166
163
|
return 0;
|
|
167
|
-
return ((_a = NETWORK_PROFILES[profile]) === null || _a === void 0 ? void 0 : _a.latencyMs) || 0;
|
|
164
|
+
return ((_a = NETWORK_PROFILES[condition.profile]) === null || _a === void 0 ? void 0 : _a.latencyMs) || 0;
|
|
168
165
|
}
|
|
169
166
|
};
|
|
170
167
|
exports.NetworkConditioningService = NetworkConditioningService;
|
|
@@ -26,9 +26,14 @@ const axios_1 = __importDefault(require("axios"));
|
|
|
26
26
|
const logger_1 = __importDefault(require("../logger"));
|
|
27
27
|
const prisma_service_1 = require("../data-service/prisma-service");
|
|
28
28
|
const typedi_1 = require("typedi");
|
|
29
|
+
const webhookEvents_1 = require("./webhookEvents");
|
|
30
|
+
/** How many sessions' failure notices are remembered, so a session ending twice sends once. */
|
|
31
|
+
const NOTIFIED_SESSIONS_KEPT = 2000;
|
|
29
32
|
let NotificationService = class NotificationService {
|
|
30
33
|
constructor(prisma) {
|
|
31
34
|
this.prisma = prisma;
|
|
35
|
+
/** Sessions whose failure was sent, oldest first (a Set keeps insertion order). */
|
|
36
|
+
this.notifiedSessions = new Set();
|
|
32
37
|
}
|
|
33
38
|
getConfigs() {
|
|
34
39
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -72,31 +77,41 @@ let NotificationService = class NotificationService {
|
|
|
72
77
|
});
|
|
73
78
|
}
|
|
74
79
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
80
|
+
* Tells the webhooks subscribed to `session_failed` that a session failed.
|
|
81
|
+
*
|
|
82
|
+
* Once per session however many times it ends: a driver crash and the
|
|
83
|
+
* client's own delete both end it, as do a heartbeat timeout and a delete.
|
|
84
|
+
* Remembered by id in memory, so a restart forgets it, which is harmless:
|
|
85
|
+
* a session does not end again after one.
|
|
86
|
+
*/
|
|
87
|
+
notifySessionFailed(session) {
|
|
88
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
89
|
+
if (this.notifiedSessions.has(session.id))
|
|
90
|
+
return;
|
|
91
|
+
this.notifiedSessions.add(session.id);
|
|
92
|
+
if (this.notifiedSessions.size > NOTIFIED_SESSIONS_KEPT) {
|
|
93
|
+
const oldest = this.notifiedSessions.values().next().value;
|
|
94
|
+
this.notifiedSessions.delete(oldest);
|
|
95
|
+
}
|
|
96
|
+
yield this.dispatchEvent('session_failed', (0, webhookEvents_1.sessionFailedPayload)(session));
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Sends a sample of `event` to a webhook the way a real one would be sent,
|
|
101
|
+
* and fails if the delivery does: "Send test" used to say it worked
|
|
102
|
+
* whatever happened, and ignored the webhook's type and template.
|
|
78
103
|
*/
|
|
79
104
|
sendTest(url_1) {
|
|
80
|
-
return __awaiter(this, arguments, void 0, function* (url, type = 'slack', payloadTemplate) {
|
|
81
|
-
yield this.sendToWebhook({ url, type, payloadTemplate: payloadTemplate !== null && payloadTemplate !== void 0 ? payloadTemplate : null },
|
|
105
|
+
return __awaiter(this, arguments, void 0, function* (url, type = 'slack', payloadTemplate, event = 'device_new') {
|
|
106
|
+
yield this.sendToWebhook({ url, type, payloadTemplate: payloadTemplate !== null && payloadTemplate !== void 0 ? payloadTemplate : null }, event, webhookEvents_1.WEBHOOK_EVENTS[event].sample());
|
|
82
107
|
});
|
|
83
108
|
}
|
|
84
109
|
/** Delivers one event. Throws when the webhook refuses or can't be reached. */
|
|
85
110
|
sendToWebhook(config, eventType, payload) {
|
|
86
111
|
return __awaiter(this, void 0, void 0, function* () {
|
|
87
|
-
//
|
|
112
|
+
// A custom template, if there is one, replaces the built-in message.
|
|
88
113
|
if (config.payloadTemplate) {
|
|
89
|
-
const
|
|
90
|
-
// JSON if the template renders to JSON, otherwise sent as text. Only
|
|
91
|
-
// the parse may fall back: one catch around both used to post again,
|
|
92
|
-
// as text, when the JSON delivery itself failed.
|
|
93
|
-
let body;
|
|
94
|
-
try {
|
|
95
|
-
body = JSON.parse(substitutedBody);
|
|
96
|
-
}
|
|
97
|
-
catch (_a) {
|
|
98
|
-
body = { text: substitutedBody };
|
|
99
|
-
}
|
|
114
|
+
const body = (0, webhookEvents_1.renderTemplate)(config.payloadTemplate, Object.assign({ eventType }, payload));
|
|
100
115
|
yield axios_1.default.post(config.url, body);
|
|
101
116
|
return;
|
|
102
117
|
}
|
|
@@ -109,17 +124,6 @@ let NotificationService = class NotificationService {
|
|
|
109
124
|
}
|
|
110
125
|
});
|
|
111
126
|
}
|
|
112
|
-
// Principal Logic: Recursive variable substitution with {{key}} support
|
|
113
|
-
substituteTemplate(template, data) {
|
|
114
|
-
return template.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
|
|
115
|
-
const keys = key.trim().split('.');
|
|
116
|
-
let value = data;
|
|
117
|
-
for (const k of keys) {
|
|
118
|
-
value = value ? value[k] : undefined;
|
|
119
|
-
}
|
|
120
|
-
return value !== undefined ? String(value) : match;
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
127
|
sendSlackMessage(url, eventType, payload) {
|
|
124
128
|
return __awaiter(this, void 0, void 0, function* () {
|
|
125
129
|
var _a, _b, _c;
|
|
@@ -131,7 +135,10 @@ let NotificationService = class NotificationService {
|
|
|
131
135
|
color = '#ff0000';
|
|
132
136
|
break;
|
|
133
137
|
case 'session_failed':
|
|
134
|
-
text =
|
|
138
|
+
text =
|
|
139
|
+
`❌ *Session Failed*: ${payload.sessionId}\n` +
|
|
140
|
+
`Reason: ${payload.failureReason || 'no reason recorded'}\n` +
|
|
141
|
+
`Device: ${payload.deviceName || payload.udid} (${payload.udid})`;
|
|
135
142
|
color = '#ff0000';
|
|
136
143
|
break;
|
|
137
144
|
case 'device_new':
|
|
@@ -25,6 +25,8 @@ const logger_1 = __importDefault(require("../logger"));
|
|
|
25
25
|
const event_manager_1 = require("../dashboard/event-manager");
|
|
26
26
|
const SessionStatus_1 = require("../types/SessionStatus");
|
|
27
27
|
const device_service_1 = require("../data-service/device-service");
|
|
28
|
+
const PhoneNetworkRestore_1 = require("./network/PhoneNetworkRestore");
|
|
29
|
+
const sessionMemory_1 = require("../sessions/sessionMemory");
|
|
28
30
|
let OrphanSweeper = class OrphanSweeper {
|
|
29
31
|
constructor() {
|
|
30
32
|
this.log = logger_1.default.scope('OrphanSweeper');
|
|
@@ -69,6 +71,11 @@ let OrphanSweeper = class OrphanSweeper {
|
|
|
69
71
|
endTime: new Date(),
|
|
70
72
|
},
|
|
71
73
|
});
|
|
74
|
+
// A session this process still drove has its phone's network put
|
|
75
|
+
// back before the phone is released; for a previous process's it is
|
|
76
|
+
// the ledger's, at boot (PhoneNetworkRestore).
|
|
77
|
+
yield typedi_1.Container.get(PhoneNetworkRestore_1.PhoneNetworkRestore).restoreSession(s.id, 'heartbeat timeout');
|
|
78
|
+
(0, sessionMemory_1.forgetSessionMemory)(s.id);
|
|
72
79
|
// This session's claim only, not every row with its udid: the phone
|
|
73
80
|
// may be another session's by now (deviceClaims.ts).
|
|
74
81
|
yield (0, device_service_1.releaseSessionDevices)(s.id);
|
|
@@ -55,6 +55,11 @@ let PortAllocator = class PortAllocator {
|
|
|
55
55
|
configure(overrides) {
|
|
56
56
|
this.ranges = Object.assign(Object.assign({}, DEFAULT_RANGES), overrides);
|
|
57
57
|
}
|
|
58
|
+
/** The ports leased for `purpose`, first and last included. */
|
|
59
|
+
rangeOf(purpose) {
|
|
60
|
+
const [start, end] = this.ranges[purpose];
|
|
61
|
+
return [start, end];
|
|
62
|
+
}
|
|
58
63
|
acquire(purpose_1, udid_1) {
|
|
59
64
|
return __awaiter(this, arguments, void 0, function* (purpose, udid, opts = {}) {
|
|
60
65
|
var _a;
|
|
@@ -32,8 +32,11 @@ const selectorKeys_1 = require("./selector-health/selectorKeys");
|
|
|
32
32
|
const log = logger_1.default.scope('SelectorVerification');
|
|
33
33
|
/**
|
|
34
34
|
* Threshold of distinct clean build_ids required to promote a Pending
|
|
35
|
-
* SelectorState row to Resolved. "Clean" means
|
|
36
|
-
*
|
|
35
|
+
* SelectorState row to Resolved. "Clean" means the selector was found without
|
|
36
|
+
* healing in the build: a findElement of the (strategy, selector) tuple that
|
|
37
|
+
* answered an element, or a findElements that answered at least one, and no
|
|
38
|
+
* heal of it. Through 2.14 a find that failed outright counted too, so a
|
|
39
|
+
* selector that was never found again was verified as fixed.
|
|
37
40
|
*/
|
|
38
41
|
const CLEAN_BUILDS_TO_RESOLVE = 3;
|
|
39
42
|
/**
|
|
@@ -127,12 +130,20 @@ let SelectorVerificationJob = class SelectorVerificationJob {
|
|
|
127
130
|
// that ran under a CI build (build_id IS NOT NULL); ad-hoc local runs
|
|
128
131
|
// intentionally don't move verification forward.
|
|
129
132
|
//
|
|
133
|
+
// A find that found the selector is one that didn't fail (`is_error`,
|
|
134
|
+
// the dashboard's record of an error answer) and didn't answer an empty
|
|
135
|
+
// list (`{"value":[]`, findElements finding nothing). The command log
|
|
136
|
+
// keeps the answer as JSON.stringify wrote it, or as the node sent it.
|
|
137
|
+
//
|
|
130
138
|
// Identifier names (PascalCase tables, camelCase createdAt) match the
|
|
131
139
|
// actual SQLite schema generated by Prisma — see
|
|
132
140
|
// `sqlite3 prisma/dev.db ".schema SessionLog"`.
|
|
133
141
|
const heals = yield this.prisma.$queryRaw `
|
|
134
142
|
SELECT s."build_id" AS build_id,
|
|
135
|
-
MAX(CASE WHEN sl."is_healed" THEN 1 ELSE 0 END) AS healed
|
|
143
|
+
MAX(CASE WHEN sl."is_healed" THEN 1 ELSE 0 END) AS healed,
|
|
144
|
+
MAX(CASE WHEN NOT sl."is_healed" AND NOT sl."is_error"
|
|
145
|
+
AND sl."response" NOT LIKE '{"value":[]%'
|
|
146
|
+
THEN 1 ELSE 0 END) AS found
|
|
136
147
|
FROM "SessionLog" sl
|
|
137
148
|
INNER JOIN "Session" s ON sl."session_id" = s."id"
|
|
138
149
|
WHERE sl."original_strategy" = ${row.original_strategy}
|
|
@@ -142,7 +153,7 @@ let SelectorVerificationJob = class SelectorVerificationJob {
|
|
|
142
153
|
AND s."build_id" IS NOT NULL
|
|
143
154
|
GROUP BY s."build_id"
|
|
144
155
|
`;
|
|
145
|
-
const cleanBuildCount = heals.filter((r) => Number(r.healed) === 0).length;
|
|
156
|
+
const cleanBuildCount = heals.filter((r) => Number(r.healed) === 0 && Number(r.found) === 1).length;
|
|
146
157
|
if (cleanBuildCount >= CLEAN_BUILDS_TO_RESOLVE) {
|
|
147
158
|
yield this.promoteToResolved(row);
|
|
148
159
|
}
|
|
@@ -78,9 +78,13 @@ const package_json_1 = __importDefault(require("../../package.json"));
|
|
|
78
78
|
const IPluginArgs_1 = require("../interfaces/IPluginArgs");
|
|
79
79
|
const config_service_1 = require("../data-service/config-service");
|
|
80
80
|
const PluginContext_1 = require("../PluginContext");
|
|
81
|
+
const SelfHealingSwitch_1 = require("./settings/SelfHealingSwitch");
|
|
82
|
+
const aiEngineSettings_1 = require("./settings/aiEngineSettings");
|
|
81
83
|
const device_store_1 = require("../data-service/device-store");
|
|
82
84
|
const device_utils_1 = require("../device-utils");
|
|
83
85
|
const localDeviceHosts_1 = require("../device-managers/localDeviceHosts");
|
|
86
|
+
const deviceSettings_1 = require("../data-service/deviceSettings");
|
|
87
|
+
const PhoneNetworkRestore_1 = require("./network/PhoneNetworkRestore");
|
|
84
88
|
const app_1 = require("../app");
|
|
85
89
|
const registerCommandAuth_1 = require("../app/registerCommandAuth");
|
|
86
90
|
const defaultGateway_1 = require("../gateway/defaultGateway");
|
|
@@ -129,10 +133,17 @@ let ServerManager = ServerManager_1 = class ServerManager {
|
|
|
129
133
|
this.logger.warn('Authentication is DISABLED — every /xenon/api caller is treated as a ' +
|
|
130
134
|
'synthetic SUPER_ADMIN. Local development only.');
|
|
131
135
|
}
|
|
136
|
+
this.applyRecordingOptions(pluginArgs);
|
|
132
137
|
yield this.syncDatabaseAndAIConfig(pluginArgs);
|
|
133
138
|
yield this.initializeCoreSubsystems(pluginArgs, cliArgs.port);
|
|
134
139
|
// Before anything below starts go-ios: the reap would kill it too.
|
|
135
140
|
yield this.reapLeftoverGoIos();
|
|
141
|
+
// The Settings page's AI self-healing toggle, which the command interceptor
|
|
142
|
+
// reads from memory at every command: load it before a command can arrive.
|
|
143
|
+
yield typedi_1.Container.get(SelfHealingSwitch_1.SelfHealingSwitch).load();
|
|
144
|
+
// The AI engine page's provider, model and base URL, over the startup
|
|
145
|
+
// options applied above (what a cleared value goes back to).
|
|
146
|
+
yield typedi_1.Container.get(aiEngineSettings_1.AiEngineSettings).load();
|
|
136
147
|
this.registerRoutes(expressApp, httpServer, cliArgs, pluginArgs);
|
|
137
148
|
yield this.bootEmulators(pluginArgs);
|
|
138
149
|
this.registerDependenciesInContainer(pluginArgs, cliArgs, nodeId);
|
|
@@ -258,6 +269,9 @@ let ServerManager = ServerManager_1 = class ServerManager {
|
|
|
258
269
|
yield (0, device_utils_1.updateDeviceList)(localHosts, pluginArgs.hub, pluginArgs.tlsRejectUnauthorized);
|
|
259
270
|
// remove stale devices
|
|
260
271
|
yield (0, device_utils_1.removeStaleDevices)(localHosts, pluginArgs.tlsRejectUnauthorized);
|
|
272
|
+
// In the background: put back the network a previous run left changed on
|
|
273
|
+
// this server's phones (an Offline profile, an interceptor proxy).
|
|
274
|
+
void this.cleanUpPhoneNetworks(localHosts, nodeId);
|
|
261
275
|
this.logger.info(`🚀 Xenon will be served at http://${pluginArgs.bindHostOrIp}:${cliArgs.port}/xenon with id ${nodeId}`);
|
|
262
276
|
this.logger.info('You can provide the following URLs to access Xenon:');
|
|
263
277
|
for (const url of (0, networkAddresses_1.listReachableBaseUrls)(cliArgs.port)) {
|
|
@@ -266,6 +280,29 @@ let ServerManager = ServerManager_1 = class ServerManager {
|
|
|
266
280
|
}
|
|
267
281
|
});
|
|
268
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* What a previous run of this server left on its phones' network: the
|
|
285
|
+
* changes its ledger holds, and on this server's own Android phones a proxy
|
|
286
|
+
* that points at a capture port of this machine where nothing answers.
|
|
287
|
+
* Each change is logged. A phone not connected now is put back at its next
|
|
288
|
+
* session here, or at the next start.
|
|
289
|
+
*/
|
|
290
|
+
cleanUpPhoneNetworks(localHosts, nodeId) {
|
|
291
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
292
|
+
var _a;
|
|
293
|
+
try {
|
|
294
|
+
const devices = yield device_store_1.DeviceStoreFactory.getStore().getAllDevices();
|
|
295
|
+
const androids = devices
|
|
296
|
+
.filter((d) => { var _a; return ((_a = d.platform) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'android' && !d.cloud; })
|
|
297
|
+
.filter((d) => (0, localDeviceHosts_1.isOwnDevice)(localHosts, nodeId, d))
|
|
298
|
+
.map((d) => d.udid);
|
|
299
|
+
yield typedi_1.Container.get(PhoneNetworkRestore_1.PhoneNetworkRestore).cleanUpAtBoot([...new Set(androids)]);
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
this.logger.warn(`Could not check the phones' network settings: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
}
|
|
269
306
|
resolvePluginArgs(cliArgs) {
|
|
270
307
|
return __awaiter(this, void 0, void 0, function* () {
|
|
271
308
|
const pluginConfigs = cliArgs.plugin;
|
|
@@ -290,6 +327,24 @@ let ServerManager = ServerManager_1 = class ServerManager {
|
|
|
290
327
|
return pluginArgs;
|
|
291
328
|
});
|
|
292
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* `maxConcurrentRecordings` and `recordingsAssetsPath` are schema options,
|
|
332
|
+
* but only their environment variables ever reached `config`. Apply the
|
|
333
|
+
* options now (option, else the variable, else the default) before the
|
|
334
|
+
* artifact store is built from the path and the recordings router first
|
|
335
|
+
* reads the cap. See `recordingConfigFrom`.
|
|
336
|
+
*/
|
|
337
|
+
applyRecordingOptions(pluginArgs) {
|
|
338
|
+
const envCap = process.env.XENON_MAX_CONCURRENT_RECORDINGS;
|
|
339
|
+
const applied = (0, config_1.recordingConfigFrom)(pluginArgs);
|
|
340
|
+
if (pluginArgs.maxConcurrentRecordings === undefined &&
|
|
341
|
+
(envCap === null || envCap === void 0 ? void 0 : envCap.trim()) &&
|
|
342
|
+
Number(envCap) !== applied.maxConcurrentRecordings) {
|
|
343
|
+
this.logger.warn(`Ignoring XENON_MAX_CONCURRENT_RECORDINGS=${JSON.stringify(envCap)}: ` +
|
|
344
|
+
`it must be a whole number of at least 1. The cap is ${applied.maxConcurrentRecordings}.`);
|
|
345
|
+
}
|
|
346
|
+
(0, config_1.updateConfig)(applied);
|
|
347
|
+
}
|
|
293
348
|
syncDatabaseAndAIConfig(pluginArgs) {
|
|
294
349
|
return __awaiter(this, void 0, void 0, function* () {
|
|
295
350
|
const { updateConfig } = yield Promise.resolve().then(() => __importStar(require('../config')));
|
|
@@ -334,14 +389,19 @@ let ServerManager = ServerManager_1 = class ServerManager {
|
|
|
334
389
|
}
|
|
335
390
|
initializeCoreSubsystems(pluginArgs, port) {
|
|
336
391
|
return __awaiter(this, void 0, void 0, function* () {
|
|
392
|
+
// Before anything opens the database: the URL must suit the database
|
|
393
|
+
// client this install was built with (SQLite for the published plugin).
|
|
394
|
+
const { assertSupportedDatabase } = yield Promise.resolve().then(() => __importStar(require('../scripts/database-check')));
|
|
395
|
+
assertSupportedDatabase();
|
|
337
396
|
yield (0, device_utils_1.initializeStorage)();
|
|
338
397
|
const { runMigrations } = yield Promise.resolve().then(() => __importStar(require('../scripts/run-migrations')));
|
|
339
398
|
yield runMigrations();
|
|
340
399
|
// A hub keeps its nodes' phones: the sessions on them outlive its restart.
|
|
341
|
-
yield device_store_1.DeviceStoreFactory.getStore()
|
|
400
|
+
yield (0, deviceSettings_1.resetDevicesAtBoot)(device_store_1.DeviceStoreFactory.getStore(), pluginArgs, (0, localDeviceHosts_1.localDeviceHosts)(pluginArgs, port));
|
|
342
401
|
const { bootstrapIdentity } = yield Promise.resolve().then(() => __importStar(require('./identity/bootstrap')));
|
|
343
402
|
yield bootstrapIdentity();
|
|
344
403
|
typedi_1.Container.get(EmailService_1.EmailService).warnIfLogFallbackEnabled();
|
|
404
|
+
typedi_1.Container.get(EmailService_1.EmailService).warnAboutPublicUrl();
|
|
345
405
|
const { startUserSessionCleanupCron } = yield Promise.resolve().then(() => __importStar(require('./identity/sessionCleanupCron')));
|
|
346
406
|
startUserSessionCleanupCron();
|
|
347
407
|
// Hub-issued JWT signing key (REST/MCP tokens, stream tickets) — key
|
|
@@ -390,7 +450,12 @@ let ServerManager = ServerManager_1 = class ServerManager {
|
|
|
390
450
|
});
|
|
391
451
|
}
|
|
392
452
|
registerRoutes(expressApp, httpServer, cliArgs, pluginArgs) {
|
|
393
|
-
expressApp.use('/xenon', (0, app_1.createRouter)(pluginArgs
|
|
453
|
+
expressApp.use('/xenon', (0, app_1.createRouter)(pluginArgs, {
|
|
454
|
+
address: cliArgs.address,
|
|
455
|
+
port: cliArgs.port,
|
|
456
|
+
// Appium serves HTTPS with both of these (base-driver's server()).
|
|
457
|
+
tls: !!(cliArgs.sslCertificatePath && cliArgs.sslKeyPath),
|
|
458
|
+
}));
|
|
394
459
|
// The session gateway goes in front of Appium's routes: Xenon's own
|
|
395
460
|
// /wd-internal calls (with the per-process secret), per-command auth
|
|
396
461
|
// (XENON_REQUIRE_COMMAND_AUTH), then, on a hub, forwarding of the sessions
|
|
@@ -417,16 +482,47 @@ let ServerManager = ServerManager_1 = class ServerManager {
|
|
|
417
482
|
`${router.describeAppiumPath()}.`);
|
|
418
483
|
}
|
|
419
484
|
}
|
|
485
|
+
/**
|
|
486
|
+
* Boot the emulators `emulators` lists, with each one's launch options.
|
|
487
|
+
*
|
|
488
|
+
* An iOS-only server, or one told to use real Android devices only
|
|
489
|
+
* (`androidDeviceType: real`, which discovery would then ignore them for),
|
|
490
|
+
* boots none. Anything else boots them, `platform: both` included: it used
|
|
491
|
+
* to boot only when `platform` named Android, so with the default it did
|
|
492
|
+
* nothing. A boot that fails is logged and the rest go on: an emulator that
|
|
493
|
+
* won't start must not keep the server from coming up with the phones it
|
|
494
|
+
* has, and with `both` counted this step now runs on many more servers.
|
|
495
|
+
*/
|
|
420
496
|
bootEmulators(pluginArgs) {
|
|
421
497
|
return __awaiter(this, void 0, void 0, function* () {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
498
|
+
const emulators = pluginArgs.emulators || [];
|
|
499
|
+
if (emulators.length === 0)
|
|
500
|
+
return;
|
|
501
|
+
const platform = String(pluginArgs.platform).toLowerCase();
|
|
502
|
+
if (platform === 'ios') {
|
|
503
|
+
this.logger.warn(`Not booting the ${emulators.length} configured emulator(s): platform is ios.`);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (pluginArgs.androidDeviceType === 'real') {
|
|
507
|
+
this.logger.warn(`Not booting the ${emulators.length} configured emulator(s): androidDeviceType is real.`);
|
|
508
|
+
return;
|
|
429
509
|
}
|
|
510
|
+
this.logger.info(`Booting ${emulators.length} configured emulator(s)...`);
|
|
511
|
+
let adb;
|
|
512
|
+
try {
|
|
513
|
+
adb = yield appium_adb_1.ADB.createADB({});
|
|
514
|
+
}
|
|
515
|
+
catch (err) {
|
|
516
|
+
this.logger.warn(`Not booting the configured emulators: no usable adb (${err === null || err === void 0 ? void 0 : err.message}).`);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
const results = yield Promise.allSettled(emulators.map((emulator) => adb.launchAVD(emulator.avdName, emulator)));
|
|
520
|
+
results.forEach((result, index) => {
|
|
521
|
+
var _a, _b;
|
|
522
|
+
if (result.status === 'rejected') {
|
|
523
|
+
this.logger.warn(`Could not boot emulator ${emulators[index].avdName}: ${(_b = (_a = result.reason) === null || _a === void 0 ? void 0 : _a.message) !== null && _b !== void 0 ? _b : result.reason}`);
|
|
524
|
+
}
|
|
525
|
+
});
|
|
430
526
|
});
|
|
431
527
|
}
|
|
432
528
|
registerDependenciesInContainer(pluginArgs, cliArgs, nodeId) {
|