@xenon-device-management/xenon 2.14.0 → 2.16.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 +44 -29
- package/lib/package.json +6 -5
- package/lib/public/assets/{AnnotationOverlay-B_VNtKBH.js → AnnotationOverlay-2W4JvEyZ.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-BwuaMjkX.js → ApiKeyGate-GmckX_7r.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-DqODzkyo.js +2 -0
- package/lib/public/assets/{EmptyState-BSjhk_IV.js → EmptyState-7hETKEWb.js} +1 -1
- package/lib/public/assets/{FieldGroup-Cb5QZsE1.js → FieldGroup-GOgssFEP.js} +1 -1
- package/lib/public/assets/{FilterMenu-BAaSlWbW.js → FilterMenu-Yvf6RGMd.js} +1 -1
- package/lib/public/assets/{Menu-JGGYrSQQ.js → Menu-KrPnfdsf.js} +1 -1
- package/lib/public/assets/{Modal-CFCp3n9d.js → Modal-C6WjZFeI.js} +1 -1
- package/lib/public/assets/{RecordingPage-CbJj_EBT.js → RecordingPage-CSRIhZEh.js} +1 -1
- package/lib/public/assets/{RecordingsPage-BJeYnygR.js → RecordingsPage-CB8ST3nE.js} +1 -1
- package/lib/public/assets/{SegmentedControl-Bg42Atjy.js → SegmentedControl-C9o90KId.js} +1 -1
- package/lib/public/assets/{SettingCard-Njz1pUIJ.js → SettingCard-CPm3w9l2.js} +1 -1
- package/lib/public/assets/{Table-BcTm5xV2.js → Table-DwIgERRt.js} +1 -1
- package/lib/public/assets/{activity-DOVdsfMQ.js → activity-DrLbHifn.js} +1 -1
- package/lib/public/assets/ai-settings-DU_Ls06P.js +16 -0
- package/lib/public/assets/{api-keys-BKnNSTw9.js → api-keys-CWYdEzJ_.js} +1 -1
- package/lib/public/assets/{apps-D0SC6uw6.js → apps-BqNVdln7.js} +1 -1
- package/lib/public/assets/{arrow-left-MMbTmgoE.js → arrow-left-BtB0_Jg_.js} +1 -1
- package/lib/public/assets/{arrow-right-BMfnGmxW.js → arrow-right-C8NhdtyN.js} +1 -1
- package/lib/public/assets/{arrow-up-right-DuAftMWB.js → arrow-up-right-gMg0ffvv.js} +1 -1
- package/lib/public/assets/{auth-shell-nZC2JNDO.js → auth-shell-CiYZ3N4M.js} +2 -2
- package/lib/public/assets/{builds-page-hDCgTwkS.js → builds-page-EaaqDmLN.js} +1 -1
- package/lib/public/assets/{button-Dda4irn_.js → button-DDau4Axv.js} +1 -1
- package/lib/public/assets/{calendar-BMr0Q62v.js → calendar-DQ83B1ed.js} +1 -1
- package/lib/public/assets/{check-CZ9iw9-e.js → check-K1ovTQam.js} +1 -1
- package/lib/public/assets/{chevron-right-BRYJJVC9.js → chevron-right-Biq00pMt.js} +1 -1
- package/lib/public/assets/{circle-check-CqAJjQ5O.js → circle-check-CBv88cH7.js} +1 -1
- package/lib/public/assets/{circle-x-DlDJtnVz.js → circle-x-Bmltailn.js} +1 -1
- package/lib/public/assets/{clock-CpuDKysU.js → clock-BPrld-3h.js} +1 -1
- package/lib/public/assets/{copy-CAQydNFe.js → copy-t0QB14JI.js} +1 -1
- package/lib/public/assets/derive-UnLRp4r3.js +1 -0
- package/lib/public/assets/device-explorer-BgGZ8AvV.js +282 -0
- package/lib/public/assets/{download-C9XG6lLv.js → download-BroHxNOh.js} +1 -1
- package/lib/public/assets/forgot-password-Bjmf49i6.js +1 -0
- package/lib/public/assets/{h264Stream-VQBphYhg.js → h264Stream-DeD__6wa.js} +6 -6
- package/lib/public/assets/{index-DJGc1M2f.js → index-72oF89LJ.js} +1 -1
- package/lib/public/assets/index-B0JeuZbg.js +15 -0
- package/lib/public/assets/index-B0gQkEg0.js +219 -0
- package/lib/public/assets/{input-Cm0tmSoc.js → input-DIc4MSC7.js} +1 -1
- package/lib/public/assets/{line-chart-D2DyJCml.js → line-chart-BcWm2vW8.js} +1 -1
- package/lib/public/assets/{list-checks-UWf1GY99.js → list-checks-B06IJJad.js} +1 -1
- package/lib/public/assets/{lock-CCJv9m_I.js → lock-pVLvnN30.js} +1 -1
- package/lib/public/assets/{login-f8vOVW-U.js → login-CN77vy--.js} +1 -1
- package/lib/public/assets/{maintenance-settings-C1PrkuAH.js → maintenance-settings-CSdzQSkY.js} +1 -1
- package/lib/public/assets/{monitor-B2jDgwm7.js → monitor-BtrN5IkI.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-DLI1RtzU.js → mouse-pointer-2-CNsQfo6w.js} +1 -1
- package/lib/public/assets/{network-ebT86qoi.js → network-BSfA8ZxG.js} +1 -1
- package/lib/public/assets/{overview-C0aykVrd.js → overview-BfTebCK5.js} +1 -1
- package/lib/public/assets/{page-header-BjzgWz-T.js → page-header-D8KUX6Qg.js} +1 -1
- package/lib/public/assets/{play-DLG9t8FU.js → play-Cvc8xzic.js} +1 -1
- package/lib/public/assets/{plus-nBFSMu_X.js → plus-C9Zj84UF.js} +1 -1
- package/lib/public/assets/{profile-page-CSl0UYlv.js → profile-page-Bi27STgs.js} +1 -1
- package/lib/public/assets/{reset-password-LeOuHqmB.js → reset-password-CPsOuwSu.js} +1 -1
- package/lib/public/assets/runbook-page-DEQ-DBOX.js +304 -0
- package/lib/public/assets/{select-88jHcl-Z.js → select-BWpnhWXj.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BbXKJjZM.js → selector-detail-redirect-BefSCSpe.js} +1 -1
- package/lib/public/assets/{selector-health-page-B4rA1Xkv.js → selector-health-page-MSk96Zz7.js} +1 -1
- package/lib/public/assets/session-detail-page-DPyBNef_.js +4 -0
- package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
- package/lib/public/assets/{settings-GF6ZMSQ6.js → settings-V6x-cXwy.js} +1 -1
- package/lib/public/assets/{stat-tile-BV98e_Sv.js → stat-tile-Duex-GZD.js} +1 -1
- package/lib/public/assets/{tablet-BiAsnFDj.js → tablet-BBcoqBmL.js} +1 -1
- package/lib/public/assets/{teams-B4-jrr-1.js → teams-ts8p5rEh.js} +1 -1
- package/lib/public/assets/{trash-2-C5RSuX0e.js → trash-2-CZ2ew6AZ.js} +1 -1
- package/lib/public/assets/{upload-BG7_rMXK.js → upload-Dv99y_le.js} +1 -1
- package/lib/public/assets/{use-builds-data-J-riQYyC.js → use-builds-data-DrYC8JCF.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-C8-eH-UG.js → use-password-reset-mode-C7QkBNkP.js} +1 -1
- package/lib/public/assets/{users-CFba5TSy.js → users-D7tKMESH.js} +1 -1
- package/lib/public/assets/{users-Cp0beJm3.js → users-gJ5Y01yo.js} +1 -1
- package/lib/public/assets/{video-off-DDsEISjV.js → video-off-CqZVuZGs.js} +1 -1
- package/lib/public/assets/{webhook-settings-BGu32vSB.js → webhook-settings-DI5DSW99.js} +1 -1
- package/lib/public/assets/{zap-l4VaXzq8.js → zap-ClcHFxfo.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +16 -12
- package/lib/src/InternalHttpClient.js +27 -5
- package/lib/src/XenonCapabilityManager.js +10 -3
- package/lib/src/app/dashboardPluginLink.js +117 -0
- package/lib/src/app/index.js +13 -34
- package/lib/src/app/openapi/control.yaml +6 -4
- package/lib/src/app/openapi/grid.yaml +8 -2
- package/lib/src/app/openapi/identity.yaml +103 -31
- package/lib/src/app/openapi/platform.yaml +33 -15
- package/lib/src/app/openapi/sessions.yaml +12 -3
- package/lib/src/app/routers/apikeys.js +14 -2
- package/lib/src/app/routers/auth.js +31 -6
- package/lib/src/app/routers/control.js +13 -5
- package/lib/src/app/routers/dashboard.js +42 -27
- package/lib/src/app/routers/grid.js +5 -3
- package/lib/src/app/routers/profile.js +21 -0
- package/lib/src/app/routers/users.js +12 -6
- package/lib/src/app/swagger.js +2 -0
- package/lib/src/app/ws/h264StreamWs.js +10 -1
- package/lib/src/app/ws/nodeSocketRelay.js +77 -11
- package/lib/src/config.js +1 -0
- package/lib/src/dashboard/commandLogFields.js +42 -0
- package/lib/src/dashboard/commands.js +5 -5
- package/lib/src/dashboard/event-manager.js +64 -204
- package/lib/src/dashboard/services/failure-analysis-service.js +118 -105
- package/lib/src/dashboard/services/failureCategories.js +264 -0
- package/lib/src/dashboard/sessionCommandSummary.js +39 -0
- package/lib/src/data-service/device-store.js +14 -1
- package/lib/src/data-service/prisma-store.js +19 -7
- package/lib/src/data-service/web-config-service.js +7 -0
- package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
- package/lib/src/device-managers/IOSDeviceManager.js +10 -0
- package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
- package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
- package/lib/src/device-utils.js +29 -3
- package/lib/src/gateway/defaultGateway.js +1 -1
- package/lib/src/gateway/forwardToNode.js +6 -11
- package/lib/src/gateway/healReport.js +87 -0
- package/lib/src/gateway/hubSessionToken.js +27 -4
- package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
- package/lib/src/gateway/sessionGateway.js +12 -2
- package/lib/src/generated/client/edge.js +4 -3
- package/lib/src/generated/client/index-browser.js +1 -0
- package/lib/src/generated/client/index.d.ts +32 -0
- package/lib/src/generated/client/index.js +4 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +3 -0
- package/lib/src/generated/client/wasm.js +1 -0
- package/lib/src/helpers/outboundProxy.js +219 -0
- package/lib/src/index.js +21 -0
- package/lib/src/interceptors/CommandInterceptor.js +256 -196
- package/lib/src/interceptors/xenonScripts.js +28 -7
- package/lib/src/logger.js +3 -1
- 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 +16 -6
- package/lib/src/scripts/database-check.js +3 -3
- package/lib/src/scripts/initialize-database.js +35 -33
- package/lib/src/scripts/run-migrations.js +1079 -27
- package/lib/src/services/AICommandService.js +9 -2
- package/lib/src/services/AIService.js +192 -42
- package/lib/src/services/ApiKeyService.js +16 -5
- package/lib/src/services/EmailService.js +29 -0
- package/lib/src/services/InterceptorService.js +6 -2
- package/lib/src/services/OrphanSweeper.js +8 -0
- package/lib/src/services/SelectorStateService.js +11 -7
- package/lib/src/services/SelectorVerificationJob.js +21 -6
- package/lib/src/services/ServerManager.js +11 -1
- package/lib/src/services/SessionLifecycleService.js +118 -44
- package/lib/src/services/SocketClient.js +11 -0
- package/lib/src/services/SocketServer.js +380 -45
- package/lib/src/services/TeamService.js +5 -1
- package/lib/src/services/TracingService.js +171 -60
- package/lib/src/services/UserService.js +14 -3
- package/lib/src/services/UserSessionService.js +16 -4
- 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 +57 -72
- 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/identity/identityChanges.js +41 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +358 -0
- package/lib/src/services/logcat/deviceLogBook.js +164 -0
- package/lib/src/services/logcat/iosDriverLog.js +178 -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 +83 -56
- 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/selector-health/SelectorVisibilityResolver.js +121 -0
- package/lib/src/services/session/sessionIdentity.js +5 -3
- package/lib/src/services/session/shutdownReason.js +26 -0
- package/lib/src/services/settings/aiEngineSettings.js +219 -0
- package/lib/src/services/token/JwtKeyService.js +8 -2
- package/lib/src/services/token/mcpTokenTtl.js +27 -0
- package/lib/src/services/token/mintedLifetime.js +51 -0
- package/lib/src/sessions/LocalSession.js +29 -1
- package/lib/src/sessions/RemoteSession.js +12 -2
- package/lib/src/sessions/SessionManager.js +4 -1
- package/lib/src/sessions/sessionMemory.js +25 -0
- package/lib/test/helpers/driver-log.js +38 -0
- package/lib/test/helpers/fake-ai-provider.js +188 -0
- package/lib/test/helpers/fake-http-proxy.js +106 -0
- package/lib/test/helpers/loki-stores.js +33 -0
- package/lib/test/helpers/otlp-probe.js +107 -0
- package/lib/test/helpers/page-sources.js +164 -0
- package/lib/test/helpers/scratch-database.js +57 -13
- package/lib/test/integration/androidDevices.spec.js +1 -1
- 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/ios/01iOSSimulator.spec.js +1 -1
- package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
- 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/selector-state-flow.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/XenonCapabilityManager.spec.js +23 -0
- 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-adb-retry.spec.js +112 -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/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/command-log-network-capture.spec.js +191 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +255 -0
- package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
- package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
- package/lib/test/unit/database-check.spec.js +1 -0
- package/lib/test/unit/device-log-book.spec.js +180 -0
- package/lib/test/unit/device-utils.spec.js +13 -4
- package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
- package/lib/test/unit/event-log-session-data.spec.js +251 -0
- package/lib/test/unit/failure-analysis.spec.js +196 -0
- package/lib/test/unit/failure-categories.spec.js +130 -0
- package/lib/test/unit/failure-categorization.spec.js +393 -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 +74 -29
- package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +243 -1
- package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
- package/lib/test/unit/ios-driver-log.spec.js +168 -0
- package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
- package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -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/logcat-ws.spec.js +78 -37
- package/lib/test/unit/logger-json-levels.spec.js +44 -0
- package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
- package/lib/test/unit/observability-example-loki.spec.js +25 -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/omni-virtual-element-store.spec.js +301 -0
- package/lib/test/unit/omni-vision-ios-points.spec.js +0 -2
- package/lib/test/unit/omni-vision.spec.js +5 -7
- package/lib/test/unit/openapi-for-site.spec.js +80 -0
- package/lib/test/unit/outbound-proxy.spec.js +320 -0
- package/lib/test/unit/package-engines.spec.js +27 -0
- package/lib/test/unit/passwordResetMode.test.js +10 -2
- package/lib/test/unit/port-exhaustion.spec.js +2 -1
- package/lib/test/unit/profile-router.test.js +122 -1
- package/lib/test/unit/proxy-option.spec.js +358 -0
- package/lib/test/unit/queue-requester.spec.js +1 -1
- 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/run-migrations-database.spec.js +807 -0
- package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
- package/lib/test/unit/run-migrations.test.js +864 -0
- package/lib/test/unit/selector-health-actions.spec.js +3 -1
- package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
- package/lib/test/unit/selector-state-service.spec.js +72 -23
- package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
- package/lib/test/unit/selector-verification-job.spec.js +28 -19
- package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
- package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
- package/lib/test/unit/self-healing-boot.spec.js +10 -5
- package/lib/test/unit/self-healing-setting.spec.js +7 -6
- 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-ios.spec.js +180 -0
- package/lib/test/unit/session-device-logs.spec.js +356 -0
- package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
- package/lib/test/unit/session-log-indexes.spec.js +64 -3
- package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
- package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
- package/lib/test/unit/stream-start-pipelines.spec.js +72 -18
- package/lib/test/unit/token-mint-limits.spec.js +165 -0
- package/lib/test/unit/tracing-export-routing.spec.js +137 -0
- package/lib/test/unit/tracing-session-spans.spec.js +337 -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 +3 -2
- package/lib/test/unit/web-config-service.spec.js +19 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -5
- package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
- package/prisma/schema.prisma +3 -0
- package/schema.json +16 -12
- package/scripts/dev/readme-screenshots.js +276 -23
- package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
- package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
- package/scripts/dev/screenshot-fixtures/session.js +242 -0
- package/scripts/export-openapi.js +32 -0
- package/scripts/lib/openapi-for-site.js +79 -0
- package/lib/public/assets/BugReportButton-BAt0_pzq.js +0 -15
- package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +0 -2
- package/lib/public/assets/ai-settings-CsuJroRf.js +0 -21
- package/lib/public/assets/device-explorer-CdO6_nrn.js +0 -282
- package/lib/public/assets/forgot-password-CGnkRgE9.js +0 -1
- package/lib/public/assets/index-BbKEXUFH.js +0 -219
- package/lib/public/assets/runbook-page-CMMeQml9.js +0 -124
- package/lib/public/assets/session-detail-page-COL_RZxw.js +0 -4
- package/lib/src/scripts/prepare-prisma.js +0 -89
- /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
|
@@ -90,6 +90,11 @@ const typedi_2 = require("typedi");
|
|
|
90
90
|
* they had worked. Within the 15 minutes a hub gives a node's install.
|
|
91
91
|
*/
|
|
92
92
|
exports.ANDROID_INSTALL_TIMEOUT_MS = 10 * 60000;
|
|
93
|
+
/** How long after adb, or its device tracking, failed to start it is tried again. */
|
|
94
|
+
const ADB_RETRY_MS = 60000;
|
|
95
|
+
function reasonOf(e) {
|
|
96
|
+
return e instanceof Error ? e.message : String(e);
|
|
97
|
+
}
|
|
93
98
|
/**
|
|
94
99
|
* Whether a server set to `androidDeviceType` serves this phone: real phones,
|
|
95
100
|
* emulators ('simulated'), or both, and with `bootedEmulators` only the
|
|
@@ -110,6 +115,11 @@ let AndroidDeviceManager = class AndroidDeviceManager {
|
|
|
110
115
|
this.context = context;
|
|
111
116
|
this.log = logger_1.default.scope('AndroidManager');
|
|
112
117
|
this.adbAvailable = true;
|
|
118
|
+
// When adb last failed to start, discovery answers no phones until this
|
|
119
|
+
// time, then tries again.
|
|
120
|
+
this.adbRetryAt = 0;
|
|
121
|
+
// When adb's device tracking last failed to start, when to try it again.
|
|
122
|
+
this.trackerRetryAt = 0;
|
|
113
123
|
this.abortControl = new Map();
|
|
114
124
|
this.tracker = undefined;
|
|
115
125
|
this.remoteTrackers = [];
|
|
@@ -161,8 +171,11 @@ let AndroidDeviceManager = class AndroidDeviceManager {
|
|
|
161
171
|
return __awaiter(this, void 0, void 0, function* () {
|
|
162
172
|
var _a;
|
|
163
173
|
if (!this.adbAvailable) {
|
|
164
|
-
|
|
165
|
-
|
|
174
|
+
if (Date.now() < this.adbRetryAt) {
|
|
175
|
+
logger_1.default.debug('adb is not available. So, returning empty list');
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
this.adbAvailable = true;
|
|
166
179
|
}
|
|
167
180
|
let devices = [];
|
|
168
181
|
try {
|
|
@@ -401,32 +414,57 @@ let AndroidDeviceManager = class AndroidDeviceManager {
|
|
|
401
414
|
}
|
|
402
415
|
getAdb() {
|
|
403
416
|
return __awaiter(this, void 0, void 0, function* () {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
417
|
+
// One start at a time: calls that come while adb or its tracker is
|
|
418
|
+
// starting wait for that start. Two discoveries at once each started a
|
|
419
|
+
// tracker, and each tracker handled every plug event again.
|
|
420
|
+
if (!this.adbStarting) {
|
|
421
|
+
this.adbStarting = this.startAdb().finally(() => {
|
|
422
|
+
this.adbStarting = undefined;
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
yield this.adbStarting;
|
|
426
|
+
return { adbInstance: this.adb, adbTracker: this.tracker };
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
startAdb() {
|
|
430
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
431
|
+
if (!this.adb) {
|
|
432
|
+
try {
|
|
433
|
+
this.adb = yield appium_adb_1.ADB.createADB({});
|
|
434
|
+
}
|
|
435
|
+
catch (e) {
|
|
436
|
+
this.adbUnavailable('Could not start adb', e);
|
|
421
437
|
}
|
|
422
438
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
439
|
+
// Plug events. Without them discovery still lists phones each time it
|
|
440
|
+
// runs, so a failure here hides none; it is tried again a minute later.
|
|
441
|
+
if (this.adb &&
|
|
442
|
+
!this.tracker &&
|
|
443
|
+
process.env.NODE_ENV !== 'test' &&
|
|
444
|
+
Date.now() >= this.trackerRetryAt) {
|
|
445
|
+
try {
|
|
446
|
+
const tracker = yield adbkit_1.default.createClient().trackDevices();
|
|
447
|
+
yield this.createLocalAdbTracker(tracker, this.adb)();
|
|
448
|
+
this.tracker = tracker;
|
|
449
|
+
}
|
|
450
|
+
catch (e) {
|
|
451
|
+
this.trackerRetryAt = Date.now() + ADB_RETRY_MS;
|
|
452
|
+
this.log.error(`Could not follow Android plug events through adb: ${reasonOf(e)}. ` +
|
|
453
|
+
`Trying again in ${ADB_RETRY_MS / 1000} s.`);
|
|
454
|
+
}
|
|
426
455
|
}
|
|
427
|
-
return { adbInstance: this.adb, adbTracker: this.tracker };
|
|
428
456
|
});
|
|
429
457
|
}
|
|
458
|
+
/**
|
|
459
|
+
* Discovery answers no phones for a minute, then tries again: adb may work
|
|
460
|
+
* by then (an SDK installed, a stuck adb server restarted). Through 2.15 it
|
|
461
|
+
* never tried again before a restart, and didn't say why.
|
|
462
|
+
*/
|
|
463
|
+
adbUnavailable(what, e) {
|
|
464
|
+
this.adbAvailable = false;
|
|
465
|
+
this.adbRetryAt = Date.now() + ADB_RETRY_MS;
|
|
466
|
+
this.log.error(`${what}: ${reasonOf(e)}. Trying again in ${ADB_RETRY_MS / 1000} s.`);
|
|
467
|
+
}
|
|
430
468
|
getAdbForDevice(udid) {
|
|
431
469
|
return __awaiter(this, void 0, void 0, function* () {
|
|
432
470
|
const { adbInstance } = yield this.getAdb();
|
|
@@ -266,6 +266,16 @@ let IOSDeviceManager = class IOSDeviceManager {
|
|
|
266
266
|
var _a;
|
|
267
267
|
if (device.cloud)
|
|
268
268
|
return true;
|
|
269
|
+
// A simulator's WebDriverAgent is the XCUITest driver's: it builds,
|
|
270
|
+
// installs and launches it on the session's wdaLocalPort (iOSCapabilities)
|
|
271
|
+
// once Appium creates the session, after this check, and boots a shut-down
|
|
272
|
+
// simulator itself. Xenon runs WDA only on an iPhone (go-ios runwda), so
|
|
273
|
+
// asking for it here refused every simulator whose WDA wasn't already up,
|
|
274
|
+
// and recoverHealth's reboot never starts one. Through 2.15 it did.
|
|
275
|
+
if (!device.realDevice) {
|
|
276
|
+
this.log.info(`[SessionStart] ${device.udid} is a simulator: the driver starts its WebDriverAgent`);
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
269
279
|
this.log.info(`🚀 [SessionStart] Verifying readiness for ${device.udid}...`);
|
|
270
280
|
const streamService = typedi_1.Container.get(IOSStreamService_1.default);
|
|
271
281
|
const streamStatus = streamService.getStreamStatus(device.udid);
|
|
@@ -93,27 +93,34 @@ let AndroidH264StreamService = class AndroidH264StreamService {
|
|
|
93
93
|
startWatchdog() {
|
|
94
94
|
// unref()ed below: the server's listener keeps the process alive; this only
|
|
95
95
|
// needs to fire while it runs.
|
|
96
|
-
setInterval(() =>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
// good. stop() itself must not release (a recording starting calls
|
|
112
|
-
// it); an idle stop does, if nothing else uses the device.
|
|
113
|
-
void this.stop(udid).then(() => (0, previewHold_1.releaseIdlePreviewHold)(udid));
|
|
114
|
-
}
|
|
96
|
+
setInterval(() => this.sweep(Date.now()), 60000).unref();
|
|
97
|
+
}
|
|
98
|
+
/** One look at every capture: ends those nobody has watched for long enough. */
|
|
99
|
+
sweep(now) {
|
|
100
|
+
for (const [udid, s] of this.sessions.entries()) {
|
|
101
|
+
if (s.status !== 'running')
|
|
102
|
+
continue;
|
|
103
|
+
if (s.mux.clientCount > 0) {
|
|
104
|
+
s.emptyAt = undefined;
|
|
105
|
+
}
|
|
106
|
+
else if (s.endWhenUnwatched && !this.startPromises.has(udid)) {
|
|
107
|
+
// A page asked for MJPEG while this capture was starting for a viewer
|
|
108
|
+
// who then never came (endWhenUnwatched leaves a start alone).
|
|
109
|
+
logger_1.default.info(`[${udid}] Stopping the unwatched H.264 stream: the phone is shown as MJPEG`);
|
|
110
|
+
void this.stop(udid);
|
|
115
111
|
}
|
|
116
|
-
|
|
112
|
+
else if (s.emptyAt === undefined) {
|
|
113
|
+
s.emptyAt = now;
|
|
114
|
+
}
|
|
115
|
+
else if (now - s.emptyAt > IDLE_TIMEOUT_MS) {
|
|
116
|
+
logger_1.default.info(`[${udid}] Stopping idle H.264 stream (no viewers for ${IDLE_TIMEOUT_MS}ms)`);
|
|
117
|
+
// The stream went, but the live-preview hold stayed: with H.264
|
|
118
|
+
// preview, a tab closed without releasing kept the device busy for
|
|
119
|
+
// good. stop() itself must not release (a recording starting calls
|
|
120
|
+
// it); an idle stop does, if nothing else uses the device.
|
|
121
|
+
void this.stop(udid).then(() => (0, previewHold_1.releaseIdlePreviewHold)(udid));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
117
124
|
}
|
|
118
125
|
getMultiplexer(udid) {
|
|
119
126
|
var _a;
|
|
@@ -131,6 +138,7 @@ let AndroidH264StreamService = class AndroidH264StreamService {
|
|
|
131
138
|
var _a, _b;
|
|
132
139
|
const mux = new H264Multiplexer_1.H264Multiplexer();
|
|
133
140
|
const session = { status: 'running', mux };
|
|
141
|
+
mux.onEmpty(() => this.endIfUnwatched(udid, session));
|
|
134
142
|
this.sessions.set(udid, session);
|
|
135
143
|
try {
|
|
136
144
|
let resolveConfig = () => undefined;
|
|
@@ -178,6 +186,44 @@ let AndroidH264StreamService = class AndroidH264StreamService {
|
|
|
178
186
|
}
|
|
179
187
|
});
|
|
180
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* A page that showed this phone's H.264 preview now shows MJPEG (its player
|
|
191
|
+
* failed, or it cannot play H.264), and the MJPEG capture is starting for
|
|
192
|
+
* it. One capture per phone where possible, but never at the expense of a
|
|
193
|
+
* viewer still playing H.264: another tile, tab or admin may be. So the
|
|
194
|
+
* capture ends now if nobody watches it, else the moment its last viewer
|
|
195
|
+
* leaves, never after the idle wait. A viewer who joins meanwhile is served
|
|
196
|
+
* and ends it in turn. A capture still starting is left to the viewer it is
|
|
197
|
+
* starting for, and the watchdog ends it if that viewer never comes.
|
|
198
|
+
*/
|
|
199
|
+
endWhenUnwatched(udid) {
|
|
200
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
201
|
+
const session = this.sessions.get(udid);
|
|
202
|
+
if (!session || session.status !== 'running')
|
|
203
|
+
return;
|
|
204
|
+
session.endWhenUnwatched = true;
|
|
205
|
+
const viewers = session.mux.clientCount;
|
|
206
|
+
if (viewers > 0) {
|
|
207
|
+
logger_1.default.info(`[${udid}] H.264 stream kept beside MJPEG for its ${viewers} viewer(s); ` +
|
|
208
|
+
'it ends when the last one leaves');
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (this.startPromises.has(udid)) {
|
|
212
|
+
logger_1.default.info(`[${udid}] H.264 stream still starting: kept for the viewer it is starting for`);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
logger_1.default.info(`[${udid}] Stopping the unwatched H.264 stream: the phone is shown as MJPEG`);
|
|
216
|
+
yield this.stop(udid);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
/** The last viewer left `session`: end it if a page asked (endWhenUnwatched). */
|
|
220
|
+
endIfUnwatched(udid, session) {
|
|
221
|
+
// A capture replaced since (a stop and a new start) is not this one.
|
|
222
|
+
if (this.sessions.get(udid) !== session || !session.endWhenUnwatched)
|
|
223
|
+
return;
|
|
224
|
+
logger_1.default.info(`[${udid}] Stopping the H.264 stream: its last viewer left, the phone is shown as MJPEG`);
|
|
225
|
+
void this.stop(udid);
|
|
226
|
+
}
|
|
181
227
|
stop(udid) {
|
|
182
228
|
return __awaiter(this, void 0, void 0, function* () {
|
|
183
229
|
var _a;
|
|
@@ -192,6 +238,9 @@ let AndroidH264StreamService = class AndroidH264StreamService {
|
|
|
192
238
|
/* best-effort */
|
|
193
239
|
}
|
|
194
240
|
this.sessions.delete(udid);
|
|
241
|
+
// Viewers still on it (a recording starting, a stream/stop) are told, so
|
|
242
|
+
// their players fall back to MJPEG instead of freezing on the last frame.
|
|
243
|
+
session.mux.close();
|
|
195
244
|
logger_1.default.info(`[${udid}] H.264 stream terminated.`);
|
|
196
245
|
});
|
|
197
246
|
}
|
|
@@ -9,6 +9,7 @@ class H264Multiplexer {
|
|
|
9
9
|
constructor() {
|
|
10
10
|
this.clients = new Set();
|
|
11
11
|
this.gop = []; // current GOP: [keyframe, ...deltas-since]
|
|
12
|
+
this.closed = false;
|
|
12
13
|
}
|
|
13
14
|
setConfig(p) {
|
|
14
15
|
this.config = p;
|
|
@@ -16,9 +17,21 @@ class H264Multiplexer {
|
|
|
16
17
|
get clientCount() {
|
|
17
18
|
return this.clients.size;
|
|
18
19
|
}
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
/** Called each time the last client leaves. */
|
|
21
|
+
onEmpty(listener) {
|
|
22
|
+
this.emptyListener = listener;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Register a client sink, and how to end it when the stream does (see
|
|
26
|
+
* `close`). Returns a remover (safe to call twice).
|
|
27
|
+
*/
|
|
28
|
+
addClient(send, end) {
|
|
29
|
+
const c = { send, started: false, end };
|
|
30
|
+
if (this.closed) {
|
|
31
|
+
// Joined a stream already stopped: end it now rather than starve it.
|
|
32
|
+
end === null || end === void 0 ? void 0 : end();
|
|
33
|
+
return () => undefined;
|
|
34
|
+
}
|
|
22
35
|
this.clients.add(c);
|
|
23
36
|
if (this.config)
|
|
24
37
|
send(this.config);
|
|
@@ -29,9 +42,30 @@ class H264Multiplexer {
|
|
|
29
42
|
c.started = true;
|
|
30
43
|
}
|
|
31
44
|
return () => {
|
|
32
|
-
|
|
45
|
+
var _a;
|
|
46
|
+
if (this.clients.delete(c) && this.clients.size === 0)
|
|
47
|
+
(_a = this.emptyListener) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
33
48
|
};
|
|
34
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* The capture stopped: end every client (its socket closes, so its player
|
|
52
|
+
* falls back to MJPEG instead of freezing on its last frame). Not an
|
|
53
|
+
* `onEmpty`: nobody left, the stream did.
|
|
54
|
+
*/
|
|
55
|
+
close() {
|
|
56
|
+
var _a;
|
|
57
|
+
this.closed = true;
|
|
58
|
+
const ending = [...this.clients];
|
|
59
|
+
this.clients.clear();
|
|
60
|
+
for (const c of ending) {
|
|
61
|
+
try {
|
|
62
|
+
(_a = c.end) === null || _a === void 0 ? void 0 : _a.call(c);
|
|
63
|
+
}
|
|
64
|
+
catch (_b) {
|
|
65
|
+
/* one client's end must not keep the others open */
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
35
69
|
/** Feed one upstream packet; fans out per the join semantics above. */
|
|
36
70
|
push(p) {
|
|
37
71
|
if (p.type === 'config') {
|
package/lib/src/device-utils.js
CHANGED
|
@@ -87,6 +87,7 @@ const device_managers_1 = require("./device-managers");
|
|
|
87
87
|
const device_service_1 = require("./data-service/device-service");
|
|
88
88
|
const deviceClaims_1 = require("./data-service/deviceClaims");
|
|
89
89
|
const PluginContext_1 = require("./PluginContext");
|
|
90
|
+
const TracingService_1 = require("./services/TracingService");
|
|
90
91
|
const logger_1 = __importDefault(require("./logger"));
|
|
91
92
|
const Platform_1 = __importDefault(require("./enums/Platform"));
|
|
92
93
|
const lodash_1 = __importDefault(require("lodash"));
|
|
@@ -250,7 +251,7 @@ function allocateDeviceForSession(capability_1, deviceTimeOutMs_1, deviceQueryIn
|
|
|
250
251
|
if (device !== null)
|
|
251
252
|
return true;
|
|
252
253
|
}
|
|
253
|
-
logger_1.default.info(`Waiting for free device. Filter: ${JSON.stringify(filters)}
|
|
254
|
+
logger_1.default.info(`Waiting for free device. Filter: ${JSON.stringify(filters)}`);
|
|
254
255
|
return false;
|
|
255
256
|
}));
|
|
256
257
|
}), { timeout, intervalBetweenAttempts });
|
|
@@ -269,7 +270,7 @@ function allocateDeviceForSession(capability_1, deviceTimeOutMs_1, deviceQueryIn
|
|
|
269
270
|
else if (possibleDevice && (0, device_service_1.isDeviceReserved)(possibleDevice)) {
|
|
270
271
|
failureReason = `Device is reserved by ${possibleDevice.reservedBy}.`;
|
|
271
272
|
}
|
|
272
|
-
throw new Error(`${failureReason}
|
|
273
|
+
throw new Error(`${failureReason} Device request: ${JSON.stringify(filterCopy)}`);
|
|
273
274
|
}
|
|
274
275
|
// We have a locked device here
|
|
275
276
|
if (device !== null) {
|
|
@@ -696,8 +697,16 @@ function releaseBlockedDevices(newCommandTimeout) {
|
|
|
696
697
|
if (timeSinceLastCmdExecuted > timeoutSeconds) {
|
|
697
698
|
// unblock regardless of whether the device has session or not
|
|
698
699
|
logger_1.default.info(`Unblocking device ${device.udid} at host ${device.host} because it has been idle for ${timeSinceLastCmdExecuted} seconds`);
|
|
700
|
+
const idleSession = (_a = device.claimSessionId) !== null && _a !== void 0 ? _a : device.session_id;
|
|
699
701
|
// Before anything below can release the phone (onSessionStopped too).
|
|
700
|
-
yield restoreIdleSessionNetwork(
|
|
702
|
+
yield restoreIdleSessionNetwork(idleSession);
|
|
703
|
+
yield forgetIdleSessionMemory(idleSession);
|
|
704
|
+
if (idleSession) {
|
|
705
|
+
typedi_1.Container.get(TracingService_1.TracingService).endSessionSpan(idleSession, {
|
|
706
|
+
failed: true,
|
|
707
|
+
reason: 'Session timed out due to inactivity',
|
|
708
|
+
});
|
|
709
|
+
}
|
|
701
710
|
// Principal Protection: If this device has an active dashboard session, stop it properly
|
|
702
711
|
if (device.session_id) {
|
|
703
712
|
const sessionId = device.session_id;
|
|
@@ -739,6 +748,23 @@ function restoreIdleSessionNetwork(sessionId) {
|
|
|
739
748
|
}
|
|
740
749
|
});
|
|
741
750
|
}
|
|
751
|
+
/**
|
|
752
|
+
* The idle session's virtual elements and autowait settings
|
|
753
|
+
* (sessionMemory.ts). Imported lazily, as above. Never throws.
|
|
754
|
+
*/
|
|
755
|
+
function forgetIdleSessionMemory(sessionId) {
|
|
756
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
757
|
+
if (!sessionId)
|
|
758
|
+
return;
|
|
759
|
+
try {
|
|
760
|
+
const { forgetSessionMemory } = yield Promise.resolve().then(() => __importStar(require('./sessions/sessionMemory')));
|
|
761
|
+
forgetSessionMemory(sessionId);
|
|
762
|
+
}
|
|
763
|
+
catch (err) {
|
|
764
|
+
logger_1.default.warn(`Could not forget idle session ${sessionId}'s elements: ${err}`);
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
}
|
|
742
768
|
function setupCronReleaseBlockedDevices(intervalMs, newCommandTimeoutSec) {
|
|
743
769
|
return __awaiter(this, void 0, void 0, function* () {
|
|
744
770
|
if (cronTimerToReleaseBlockedDevices) {
|
|
@@ -34,7 +34,7 @@ const createWith = (hubGrants) => ({
|
|
|
34
34
|
});
|
|
35
35
|
const dashboardHooks = {
|
|
36
36
|
before: (sessionId, command, req, res) => event_manager_1.DASHBORD_EVENT_MANAGER.beforeSessionCommand(sessionId, command, req, res),
|
|
37
|
-
after: (sessionId, command, req, res, body) => event_manager_1.DASHBORD_EVENT_MANAGER.afterSessionCommand(sessionId, command, null, req, res, body),
|
|
37
|
+
after: (sessionId, command, req, res, body, heal) => event_manager_1.DASHBORD_EVENT_MANAGER.afterSessionCommand(sessionId, command, null, req, res, body, heal),
|
|
38
38
|
};
|
|
39
39
|
/** A hub: sessions other servers run are forwarded to them. */
|
|
40
40
|
function hubGatewayOptions(config) {
|
|
@@ -11,10 +11,10 @@ exports.readAnswer = readAnswer;
|
|
|
11
11
|
exports.sendAnswer = sendAnswer;
|
|
12
12
|
const http_1 = __importDefault(require("http"));
|
|
13
13
|
const https_1 = __importDefault(require("https"));
|
|
14
|
-
const https_proxy_agent_1 = require("https-proxy-agent");
|
|
15
|
-
const http_proxy_agent_1 = require("http-proxy-agent");
|
|
16
14
|
const internalCall_1 = require("./internalCall");
|
|
17
15
|
const hubSessionToken_1 = require("./hubSessionToken");
|
|
16
|
+
const healReport_1 = require("./healReport");
|
|
17
|
+
const outboundProxy_1 = require("../helpers/outboundProxy");
|
|
18
18
|
/**
|
|
19
19
|
* Sending a session command on to the node that runs the session, and handing
|
|
20
20
|
* the node's answer back to the client unchanged.
|
|
@@ -44,6 +44,7 @@ const NEVER_FORWARDED = new Set([
|
|
|
44
44
|
'x-xenon-token',
|
|
45
45
|
internalCall_1.INTERNAL_CALL_HEADER,
|
|
46
46
|
hubSessionToken_1.HUB_TOKEN_HEADER,
|
|
47
|
+
healReport_1.HEAL_REPORT_HEADER,
|
|
47
48
|
]);
|
|
48
49
|
function forwardedRequestHeaders(incoming) {
|
|
49
50
|
const out = {};
|
|
@@ -58,17 +59,11 @@ function forwardedRequestHeaders(incoming) {
|
|
|
58
59
|
out['accept-encoding'] = 'identity';
|
|
59
60
|
return out;
|
|
60
61
|
}
|
|
61
|
-
/** HTTP(S)_PROXY from the environment, as the old command proxy honoured it. */
|
|
62
|
-
function proxyAgentFor(url) {
|
|
63
|
-
const proxy = process.env.HTTP_PROXY || process.env.HTTPS_PROXY;
|
|
64
|
-
if (!proxy)
|
|
65
|
-
return undefined;
|
|
66
|
-
return url.protocol === 'https:' ? new https_proxy_agent_1.HttpsProxyAgent(proxy) : new http_proxy_agent_1.HttpProxyAgent(proxy);
|
|
67
|
-
}
|
|
68
62
|
/**
|
|
69
63
|
* Send one request to a node and resolve with its response stream. No retry:
|
|
70
64
|
* a WebDriver command is not safe to repeat. Credentials in the URL (a cloud
|
|
71
|
-
* provider's) become basic auth.
|
|
65
|
+
* provider's) become basic auth. Through the environment's proxy for the URL,
|
|
66
|
+
* by axios's rule (helpers/outboundProxy.ts).
|
|
72
67
|
*/
|
|
73
68
|
function sendToNode(request) {
|
|
74
69
|
const url = new URL(request.url);
|
|
@@ -78,7 +73,7 @@ function sendToNode(request) {
|
|
|
78
73
|
if (Buffer.isBuffer(body))
|
|
79
74
|
headers['content-length'] = body.length;
|
|
80
75
|
return new Promise((resolve, reject) => {
|
|
81
|
-
const outgoing = lib.request(url, { method: request.method, headers, agent: proxyAgentFor(url), signal: request.signal }, resolve);
|
|
76
|
+
const outgoing = lib.request(url, { method: request.method, headers, agent: (0, outboundProxy_1.proxyAgentFor)(url), signal: request.signal }, resolve);
|
|
82
77
|
outgoing.on('error', reject);
|
|
83
78
|
if (body && !Buffer.isBuffer(body)) {
|
|
84
79
|
body.on('error', (err) => outgoing.destroy(err));
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HEAL_REPORT_HEADER = void 0;
|
|
4
|
+
exports.runReportingHeals = runReportingHeals;
|
|
5
|
+
exports.reportHeal = reportHeal;
|
|
6
|
+
exports.takeHealReport = takeHealReport;
|
|
7
|
+
const async_hooks_1 = require("async_hooks");
|
|
8
|
+
/**
|
|
9
|
+
* A heal on a node's phone, on its way back to the hub.
|
|
10
|
+
*
|
|
11
|
+
* A session the hub created runs its commands on the node, so the node's
|
|
12
|
+
* CommandInterceptor heals its finds. The node keeps no record of such a
|
|
13
|
+
* session; the hub does. So the node puts the heal on its answer to the
|
|
14
|
+
* forwarded command, as the `x-xenon-heal` header, and the hub takes it off
|
|
15
|
+
* before relaying the answer and records it with the command, as it records a
|
|
16
|
+
* local session's heal.
|
|
17
|
+
*
|
|
18
|
+
* The node's session gateway runs a command that came from a hub inside
|
|
19
|
+
* `runReportingHeals`, so the interceptor, deep inside Appium's route, finds
|
|
20
|
+
* the answer to put it on. Anywhere else `reportHeal` does nothing, and the
|
|
21
|
+
* server records the heal itself.
|
|
22
|
+
*/
|
|
23
|
+
exports.HEAL_REPORT_HEADER = 'x-xenon-heal';
|
|
24
|
+
/**
|
|
25
|
+
* A header longer than this isn't sent: the hub's HTTP parser refuses an
|
|
26
|
+
* answer whose headers pass 16 KB, and a refused answer would fail the
|
|
27
|
+
* command. A heal of selectors that long goes unrecorded instead.
|
|
28
|
+
*/
|
|
29
|
+
const MAX_HEADER_LENGTH = 8 * 1024;
|
|
30
|
+
const answers = new async_hooks_1.AsyncLocalStorage();
|
|
31
|
+
/** Run the rest of a command that came from a hub with its answer at hand. */
|
|
32
|
+
function runReportingHeals(res, fn) {
|
|
33
|
+
const answer = { res };
|
|
34
|
+
const forget = () => {
|
|
35
|
+
answer.res = null;
|
|
36
|
+
};
|
|
37
|
+
res.once('finish', forget);
|
|
38
|
+
res.once('close', forget);
|
|
39
|
+
return answers.run(answer, fn);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Put `heal` on the answer to the hub's command this code runs for.
|
|
43
|
+
* `not-from-hub` when the command didn't come from a hub (or the answer has
|
|
44
|
+
* gone): the caller records the heal itself.
|
|
45
|
+
*/
|
|
46
|
+
function reportHeal(heal) {
|
|
47
|
+
var _a;
|
|
48
|
+
const res = (_a = answers.getStore()) === null || _a === void 0 ? void 0 : _a.res;
|
|
49
|
+
if (!res || res.headersSent)
|
|
50
|
+
return 'not-from-hub';
|
|
51
|
+
const encoded = Buffer.from(JSON.stringify(heal), 'utf8').toString('base64url');
|
|
52
|
+
if (encoded.length > MAX_HEADER_LENGTH)
|
|
53
|
+
return 'too-long';
|
|
54
|
+
res.setHeader(exports.HEAL_REPORT_HEADER, encoded);
|
|
55
|
+
return 'reported';
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The heal a node put on its answer, taken off the headers so it isn't
|
|
59
|
+
* relayed to the client. Undefined when there is none, or it isn't one.
|
|
60
|
+
*/
|
|
61
|
+
function takeHealReport(headers) {
|
|
62
|
+
const raw = headers[exports.HEAL_REPORT_HEADER];
|
|
63
|
+
delete headers[exports.HEAL_REPORT_HEADER];
|
|
64
|
+
if (typeof raw !== 'string' || !raw)
|
|
65
|
+
return undefined;
|
|
66
|
+
try {
|
|
67
|
+
const heal = JSON.parse(Buffer.from(raw, 'base64url').toString('utf8'));
|
|
68
|
+
if (!heal ||
|
|
69
|
+
typeof heal.originalSelector !== 'string' ||
|
|
70
|
+
typeof heal.healedSelector !== 'string' ||
|
|
71
|
+
typeof heal.confidence !== 'number') {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
const optional = (v) => (typeof v === 'string' ? v : undefined);
|
|
75
|
+
return {
|
|
76
|
+
originalSelector: heal.originalSelector,
|
|
77
|
+
originalStrategy: optional(heal.originalStrategy),
|
|
78
|
+
healedSelector: heal.healedSelector,
|
|
79
|
+
healedStrategy: optional(heal.healedStrategy),
|
|
80
|
+
confidence: heal.confidence,
|
|
81
|
+
tier: typeof heal.tier === 'number' ? heal.tier : undefined,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
catch (_a) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -59,6 +59,7 @@ const jose = __importStar(require("jose"));
|
|
|
59
59
|
const logger_1 = __importDefault(require("../logger"));
|
|
60
60
|
const JwtKeyService_1 = require("../services/token/JwtKeyService");
|
|
61
61
|
const singleUseLedger_1 = require("../services/token/singleUseLedger");
|
|
62
|
+
const outboundProxy_1 = require("../helpers/outboundProxy");
|
|
62
63
|
/**
|
|
63
64
|
* The credential a hub's calls to a node carry.
|
|
64
65
|
*
|
|
@@ -221,6 +222,22 @@ exports.HubSessionTokenIssuer = HubSessionTokenIssuer = __decorate([
|
|
|
221
222
|
function hubJwksUrl(hub) {
|
|
222
223
|
return new URL('/xenon/api/auth/jwks.json', hub);
|
|
223
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* A key lookup that fails after `ms`. jose's own timeout starts once its
|
|
227
|
+
* request has a socket, which a proxy agent hands it only after the proxy
|
|
228
|
+
* answers, so a proxy that never answers held every token check.
|
|
229
|
+
*/
|
|
230
|
+
function withinTime(getKey, ms) {
|
|
231
|
+
return (header, token) => {
|
|
232
|
+
let timer;
|
|
233
|
+
return Promise.race([
|
|
234
|
+
Promise.resolve(getKey(header, token)),
|
|
235
|
+
new Promise((_, reject) => {
|
|
236
|
+
timer = setTimeout(() => reject(new Error(`the hub's keys did not arrive in ${ms} ms`)), ms);
|
|
237
|
+
}),
|
|
238
|
+
]).finally(() => clearTimeout(timer));
|
|
239
|
+
};
|
|
240
|
+
}
|
|
224
241
|
/** The node could not check a hub token (the hub's keys could not be fetched). */
|
|
225
242
|
class HubTokenUnavailableError extends Error {
|
|
226
243
|
}
|
|
@@ -251,15 +268,21 @@ function isVerdict(err) {
|
|
|
251
268
|
* a token names a key it does not have (a hub that rotated its key).
|
|
252
269
|
*/
|
|
253
270
|
class HubSessionTokenVerifier {
|
|
254
|
-
constructor(hubUrl, keys) {
|
|
271
|
+
constructor(hubUrl, keys, opts = {}) {
|
|
272
|
+
var _a;
|
|
255
273
|
/** The create tokens already taken, by id, until each one expires. */
|
|
256
274
|
this.usedCreates = new singleUseLedger_1.SingleUseLedger(exports.HUB_CREATE_TTL_SECONDS);
|
|
275
|
+
const jwksUrl = hubJwksUrl(hubUrl);
|
|
276
|
+
const timeoutMs = (_a = opts.timeoutMs) !== null && _a !== void 0 ? _a : 5000;
|
|
257
277
|
this.keys =
|
|
258
|
-
keys !== null && keys !== void 0 ? keys : jose.createRemoteJWKSet(
|
|
259
|
-
timeoutDuration:
|
|
278
|
+
keys !== null && keys !== void 0 ? keys : withinTime(jose.createRemoteJWKSet(jwksUrl, {
|
|
279
|
+
timeoutDuration: timeoutMs,
|
|
260
280
|
cooldownDuration: 30000,
|
|
261
281
|
cacheMaxAge: 10 * 60000,
|
|
262
|
-
|
|
282
|
+
// The proxy the node's axios calls to its hub take; jose's own
|
|
283
|
+
// fetch takes none.
|
|
284
|
+
agent: (0, outboundProxy_1.proxyAgentFor)(jwksUrl),
|
|
285
|
+
}), timeoutMs);
|
|
263
286
|
}
|
|
264
287
|
verify(token, sessionId) {
|
|
265
288
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -30,6 +30,7 @@ const logger_1 = __importDefault(require("../logger"));
|
|
|
30
30
|
const helpers_1 = require("../helpers");
|
|
31
31
|
const appiumBasePath_1 = require("../app/appiumBasePath");
|
|
32
32
|
const PluginContext_1 = require("../PluginContext");
|
|
33
|
+
const outboundProxy_1 = require("../helpers/outboundProxy");
|
|
33
34
|
/**
|
|
34
35
|
* Where a node's WebDriver API lives.
|
|
35
36
|
*
|
|
@@ -95,11 +96,8 @@ let NodeBasePathResolver = class NodeBasePathResolver {
|
|
|
95
96
|
let advertised;
|
|
96
97
|
try {
|
|
97
98
|
const rejectUnauthorized = (_a = typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs) === null || _a === void 0 ? void 0 : _a.tlsRejectUnauthorized;
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
validateStatus: (status) => status === 200,
|
|
101
|
-
httpsAgent: new https_1.default.Agent({ rejectUnauthorized: rejectUnauthorized !== false }),
|
|
102
|
-
});
|
|
99
|
+
const url = `${origin}${exports.WEBDRIVER_INFO_PATH}`;
|
|
100
|
+
const response = yield axios_1.default.get(url, Object.assign({ timeout: exports.LOOKUP_TIMEOUT_MS, validateStatus: (status) => status === 200, httpsAgent: new https_1.default.Agent({ rejectUnauthorized: rejectUnauthorized !== false }) }, (0, outboundProxy_1.axiosProxyConfig)(url, { rejectUnauthorized: rejectUnauthorized !== false })));
|
|
103
101
|
const basePath = (_b = response.data) === null || _b === void 0 ? void 0 : _b.basePath;
|
|
104
102
|
if (typeof basePath === 'string')
|
|
105
103
|
advertised = (0, appiumBasePath_1.normalizeBasePath)(basePath);
|
|
@@ -15,6 +15,7 @@ exports.createHubRouting = createHubRouting;
|
|
|
15
15
|
const commandAuth_1 = require("../middleware/commandAuth");
|
|
16
16
|
const internalCall_1 = require("./internalCall");
|
|
17
17
|
const hubSessionToken_1 = require("./hubSessionToken");
|
|
18
|
+
const healReport_1 = require("./healReport");
|
|
18
19
|
const forwardToNode_1 = require("./forwardToNode");
|
|
19
20
|
/** Where a session runs could not be determined (a database error). */
|
|
20
21
|
exports.ROUTING_UNAVAILABLE_STATUS = 503;
|
|
@@ -62,7 +63,14 @@ function createSessionGatewayLayer(deps) {
|
|
|
62
63
|
const latency = (_b = (_a = deps.latencyOf) === null || _a === void 0 ? void 0 : _a.call(deps, sessionId)) !== null && _b !== void 0 ? _b : 0;
|
|
63
64
|
if (latency > 0)
|
|
64
65
|
yield new Promise((resolve) => setTimeout(resolve, latency));
|
|
65
|
-
|
|
66
|
+
// On a node, a heal of the hub's command goes back to the hub on the answer
|
|
67
|
+
// (healReport.ts). The header proves nothing elsewhere: a hub or a
|
|
68
|
+
// standalone server checks no hub token, so a client could send one there
|
|
69
|
+
// to keep its heals out of the record.
|
|
70
|
+
if (fromHub && deps.hubTokens)
|
|
71
|
+
(0, healReport_1.runReportingHeals)(res, next);
|
|
72
|
+
else
|
|
73
|
+
next();
|
|
66
74
|
});
|
|
67
75
|
}
|
|
68
76
|
return function xenonSessionGateway(req, res, next) {
|
|
@@ -176,8 +184,10 @@ function createHubRouting(deps) {
|
|
|
176
184
|
abort.abort();
|
|
177
185
|
});
|
|
178
186
|
let captured;
|
|
187
|
+
let heal;
|
|
179
188
|
try {
|
|
180
189
|
const upstream = yield (0, forwardToNode_1.sendToNode)(yield nodeRequest(req, sessionId, location, abort.signal));
|
|
190
|
+
heal = (0, healReport_1.takeHealReport)(upstream.headers);
|
|
181
191
|
captured = yield (0, forwardToNode_1.relayAnswer)(upstream, res, !!hooks);
|
|
182
192
|
}
|
|
183
193
|
catch (error) {
|
|
@@ -192,7 +202,7 @@ function createHubRouting(deps) {
|
|
|
192
202
|
}
|
|
193
203
|
if (hooks && captured !== undefined) {
|
|
194
204
|
yield hooks
|
|
195
|
-
.after(sessionId, command, req, res, captured)
|
|
205
|
+
.after(sessionId, command, req, res, captured, heal)
|
|
196
206
|
.catch((error) => logger.warn(`Dashboard log failed for session ${sessionId}: ${(0, commandAuth_1.summarize)(error)}`));
|
|
197
207
|
}
|
|
198
208
|
});
|