@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
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
3
|
+
var t = {};
|
|
4
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
5
|
+
t[p] = s[p];
|
|
6
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
7
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
8
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
9
|
+
t[p[i]] = s[p[i]];
|
|
10
|
+
}
|
|
11
|
+
return t;
|
|
12
|
+
};
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.MIN_LEAD = exports.MIN_SCORE = exports.RESILIO_PATH_FORMAT = void 0;
|
|
15
|
+
exports.resilioPathOf = resilioPathOf;
|
|
16
|
+
exports.isResilioPath = isResilioPath;
|
|
17
|
+
exports.findLearntElement = findLearntElement;
|
|
18
|
+
exports.nearestElement = nearestElement;
|
|
19
|
+
exports.locatorsSelectingOnly = locatorsSelectingOnly;
|
|
20
|
+
exports.contradictsSelector = contradictsSelector;
|
|
21
|
+
const xmldom_1 = require("@xmldom/xmldom");
|
|
22
|
+
const resiliotree_1 = require("resiliotree");
|
|
23
|
+
const xpath_1 = require("xpath");
|
|
24
|
+
/**
|
|
25
|
+
* The Resilio healing tier's view of a page source: an element's path from
|
|
26
|
+
* the root of the driver's element tree, as resiliotree compares paths.
|
|
27
|
+
*
|
|
28
|
+
* The page source is parsed as the XML it is. Through 2.14 it went through
|
|
29
|
+
* resiliotree's HTML parser, which lowercases every tag
|
|
30
|
+
* (`android.widget.button`, which the drivers' XPath doesn't match) and nests
|
|
31
|
+
* every element written `<x/>` around its next siblings.
|
|
32
|
+
*
|
|
33
|
+
* A path is stored with the selector's fingerprint (`LocatorEtalon.path`).
|
|
34
|
+
* Each node keeps what resiliotree compares (tag, id, labels, index, other
|
|
35
|
+
* attributes), not its children or text, so a path is a few kilobytes.
|
|
36
|
+
*/
|
|
37
|
+
/** Marks a path made here. A path without it (an HTML parse, before 2.15) is not used. */
|
|
38
|
+
exports.RESILIO_PATH_FORMAT = 'page-source-1';
|
|
39
|
+
/**
|
|
40
|
+
* How sure Resilio must be. An element that kept its id scores about 0.99
|
|
41
|
+
* after moving anywhere in the tree; once its id changed, or it is gone, the
|
|
42
|
+
* best element scores about 0.55, and a neighbour of the same type comes
|
|
43
|
+
* within a few hundredths of it. Below this, or with a second element as
|
|
44
|
+
* close as MIN_LEAD, the next tier decides.
|
|
45
|
+
*/
|
|
46
|
+
exports.MIN_SCORE = 0.8;
|
|
47
|
+
exports.MIN_LEAD = 0.05;
|
|
48
|
+
function platformOf(pageSource) {
|
|
49
|
+
if (/<XCUIElementType/.test(pageSource))
|
|
50
|
+
return 'ios';
|
|
51
|
+
if (/<hierarchy[\s>]/.test(pageSource))
|
|
52
|
+
return 'android';
|
|
53
|
+
return 'other';
|
|
54
|
+
}
|
|
55
|
+
const isElement = (n) => !!n && n.nodeType === 1;
|
|
56
|
+
function childElements(el) {
|
|
57
|
+
const out = [];
|
|
58
|
+
for (let c = el.firstChild; c; c = c.nextSibling)
|
|
59
|
+
if (isElement(c))
|
|
60
|
+
out.push(c);
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* One element as a resiliotree node, without children. The platform decides
|
|
65
|
+
* what its id is: Android's resource-id, iOS's name, else `id`.
|
|
66
|
+
*/
|
|
67
|
+
function nodeBuilderFor(el, index, platform) {
|
|
68
|
+
const attributes = new Map();
|
|
69
|
+
for (let i = 0; i < el.attributes.length; i++) {
|
|
70
|
+
attributes.set(el.attributes[i].name, el.attributes[i].value);
|
|
71
|
+
}
|
|
72
|
+
const builder = new resiliotree_1.NodeBuilder().setTag(el.tagName).setIndex(index);
|
|
73
|
+
if (platform === 'android')
|
|
74
|
+
builder.setAndroidAttributes(attributes);
|
|
75
|
+
else if (platform === 'ios')
|
|
76
|
+
builder.setIOSAttributes(attributes);
|
|
77
|
+
else
|
|
78
|
+
builder.setAttributes(attributes);
|
|
79
|
+
return builder;
|
|
80
|
+
}
|
|
81
|
+
function parse(pageSource) {
|
|
82
|
+
return new xmldom_1.DOMParser({
|
|
83
|
+
errorHandler: { warning: () => { }, error: () => { }, fatalError: () => { } },
|
|
84
|
+
}).parseFromString(pageSource, 'text/xml');
|
|
85
|
+
}
|
|
86
|
+
/** The page source as a resiliotree tree, each node knowing its element. */
|
|
87
|
+
function treeOf(pageSource) {
|
|
88
|
+
const doc = parse(pageSource);
|
|
89
|
+
const top = doc === null || doc === void 0 ? void 0 : doc.documentElement;
|
|
90
|
+
if (!isElement(top))
|
|
91
|
+
return null;
|
|
92
|
+
const platform = platformOf(pageSource);
|
|
93
|
+
const elementOf = new Map();
|
|
94
|
+
const build = (el, index) => {
|
|
95
|
+
const builder = nodeBuilderFor(el, index, platform);
|
|
96
|
+
childElements(el).forEach((child, i) => builder.addChild(build(child, i)));
|
|
97
|
+
const node = builder.build();
|
|
98
|
+
elementOf.set(node, el);
|
|
99
|
+
return node;
|
|
100
|
+
};
|
|
101
|
+
return { root: build(top, 0), elementOf };
|
|
102
|
+
}
|
|
103
|
+
function compact(node) {
|
|
104
|
+
const _a = node.toJSON(), { children: _children, innerText: _text } = _a, rest = __rest(_a, ["children", "innerText"]);
|
|
105
|
+
return rest;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The path of an element of a parsed page source (an xmldom Element, as the
|
|
109
|
+
* Fuzzy XML and Resilio tiers find them), from the root of the page source.
|
|
110
|
+
* `pageSource` decides the platform. Null for anything that isn't such an
|
|
111
|
+
* element.
|
|
112
|
+
*/
|
|
113
|
+
function resilioPathOf(element, pageSource) {
|
|
114
|
+
if (!isElement(element))
|
|
115
|
+
return null;
|
|
116
|
+
const platform = platformOf(pageSource);
|
|
117
|
+
const lineage = [];
|
|
118
|
+
for (let el = element; isElement(el); el = el.parentNode)
|
|
119
|
+
lineage.unshift(el);
|
|
120
|
+
const nodes = lineage.map((el) => {
|
|
121
|
+
const parent = el.parentNode;
|
|
122
|
+
const index = isElement(parent) ? childElements(parent).indexOf(el) : 0;
|
|
123
|
+
return compact(nodeBuilderFor(el, index, platform).build());
|
|
124
|
+
});
|
|
125
|
+
return { format: exports.RESILIO_PATH_FORMAT, nodes };
|
|
126
|
+
}
|
|
127
|
+
/** Whether `path` is one made here (not an older HTML parse). */
|
|
128
|
+
function isResilioPath(path) {
|
|
129
|
+
return (!!path &&
|
|
130
|
+
typeof path === 'object' &&
|
|
131
|
+
path.format === exports.RESILIO_PATH_FORMAT &&
|
|
132
|
+
Array.isArray(path.nodes) &&
|
|
133
|
+
path.nodes.length > 0);
|
|
134
|
+
}
|
|
135
|
+
/** Attributes that say which element an element is, not where it is or what state it is in. */
|
|
136
|
+
const IDENTITY_ATTRIBUTES = new Set([
|
|
137
|
+
'resource-id',
|
|
138
|
+
'content-desc',
|
|
139
|
+
'text',
|
|
140
|
+
'name',
|
|
141
|
+
'label',
|
|
142
|
+
'value',
|
|
143
|
+
'id',
|
|
144
|
+
'hint',
|
|
145
|
+
'accessibility-id',
|
|
146
|
+
]);
|
|
147
|
+
/**
|
|
148
|
+
* The element of the page source that the driver found, from its tag and the
|
|
149
|
+
* attributes Xenon read from it (`name`/`value` pairs: resource-id, text,
|
|
150
|
+
* name, label, ..., and x, y, width, height). The element of that tag with
|
|
151
|
+
* the most of them, among those that share at least one identity attribute or
|
|
152
|
+
* the whole rect with it: the page source is read after the find answered,
|
|
153
|
+
* so the screen may have changed, and a size alone names nothing. Null when
|
|
154
|
+
* none qualifies, or two qualify equally well.
|
|
155
|
+
*/
|
|
156
|
+
function findLearntElement(pageSource, nodeName, attributes) {
|
|
157
|
+
const doc = parse(pageSource);
|
|
158
|
+
if (!isElement(doc === null || doc === void 0 ? void 0 : doc.documentElement))
|
|
159
|
+
return null;
|
|
160
|
+
const tag = (nodeName || '').toLowerCase();
|
|
161
|
+
const anyTag = !tag || tag === 'xcuielementtypeany' || tag === 'unknown';
|
|
162
|
+
const wanted = new Map(attributes.map((a) => [a.name.toLowerCase(), String(a.value)]));
|
|
163
|
+
const rect = ['x', 'y', 'width', 'height'].map((k) => wanted.get(k));
|
|
164
|
+
const hasRect = rect.every((v) => v !== undefined);
|
|
165
|
+
/** How many of the attributes `el` shares, or 0 when it shares no identity and not the rect. */
|
|
166
|
+
const matchesOf = (el) => {
|
|
167
|
+
let all = 0;
|
|
168
|
+
let identity = 0;
|
|
169
|
+
for (const [name, value] of wanted) {
|
|
170
|
+
if (el.getAttribute(name) !== value)
|
|
171
|
+
continue;
|
|
172
|
+
all++;
|
|
173
|
+
if (IDENTITY_ATTRIBUTES.has(name))
|
|
174
|
+
identity++;
|
|
175
|
+
}
|
|
176
|
+
// Android writes a rect as bounds="[x1,y1][x2,y2]"; iOS as x, y, width, height.
|
|
177
|
+
let wholeRect = hasRect && ['x', 'y', 'width', 'height'].every((k, i) => el.getAttribute(k) === rect[i]);
|
|
178
|
+
const bounds = el.getAttribute('bounds');
|
|
179
|
+
if (bounds && hasRect) {
|
|
180
|
+
const [x, y, w, h] = rect.map(Number);
|
|
181
|
+
if (bounds === `[${x},${y}][${x + w},${y + h}]`) {
|
|
182
|
+
all++;
|
|
183
|
+
wholeRect = true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return identity > 0 || wholeRect ? all : 0;
|
|
187
|
+
};
|
|
188
|
+
let best = null;
|
|
189
|
+
let bestCount = 0;
|
|
190
|
+
let tie = false;
|
|
191
|
+
const walk = (el) => {
|
|
192
|
+
if (anyTag || el.tagName.toLowerCase() === tag) {
|
|
193
|
+
const n = matchesOf(el);
|
|
194
|
+
if (n > bestCount) {
|
|
195
|
+
best = el;
|
|
196
|
+
bestCount = n;
|
|
197
|
+
tie = false;
|
|
198
|
+
}
|
|
199
|
+
else if (n > 0 && n === bestCount) {
|
|
200
|
+
tie = true;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
childElements(el).forEach(walk);
|
|
204
|
+
};
|
|
205
|
+
walk(doc.documentElement);
|
|
206
|
+
return tie ? null : best;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* The element of `pageSource` at the end of the path most like `path`, and
|
|
210
|
+
* how alike they are (0 to 1). Null when no element scores MIN_SCORE, or
|
|
211
|
+
* another comes within MIN_LEAD of the best: Resilio then has no answer.
|
|
212
|
+
*/
|
|
213
|
+
function nearestElement(path, pageSource) {
|
|
214
|
+
const tree = treeOf(pageSource);
|
|
215
|
+
if (!tree)
|
|
216
|
+
return null;
|
|
217
|
+
const finder = new resiliotree_1.PathFinder(new resiliotree_1.LCSPathDistance(), new resiliotree_1.HeuristicNodeDistance());
|
|
218
|
+
// resiliotree scores an element once for each leaf below it, so an element
|
|
219
|
+
// holding others (a list row) comes back several times at one score. Every
|
|
220
|
+
// result, best first, and the best of each element.
|
|
221
|
+
const scored = finder.find(resiliotree_1.Path.fromJSON(path), tree.root, Number.MAX_SAFE_INTEGER);
|
|
222
|
+
const best = scored[0];
|
|
223
|
+
if (!best || best.score < exports.MIN_SCORE)
|
|
224
|
+
return null;
|
|
225
|
+
const second = scored.find((s) => s.value !== best.value);
|
|
226
|
+
if (second && best.score - second.score < exports.MIN_LEAD)
|
|
227
|
+
return null;
|
|
228
|
+
const element = tree.elementOf.get(best.value);
|
|
229
|
+
return element ? { element, score: best.score } : null;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* The locators among `candidates` that select `element` and nothing else in
|
|
233
|
+
* its page source. A driver answers a find with the first match, so a
|
|
234
|
+
* locator that also matches an element before it would hand back that one.
|
|
235
|
+
*/
|
|
236
|
+
function locatorsSelectingOnly(element, candidates) {
|
|
237
|
+
const doc = element.ownerDocument;
|
|
238
|
+
return candidates.filter((xpath) => {
|
|
239
|
+
try {
|
|
240
|
+
const found = (0, xpath_1.select)(xpath, doc);
|
|
241
|
+
return Array.isArray(found) && found.length === 1 && found[0] === element;
|
|
242
|
+
}
|
|
243
|
+
catch (_a) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
/** The text-like attributes a selector can state that the element it means must have. */
|
|
249
|
+
const STATED_ATTRIBUTES = 'text|content-desc|label|name|value';
|
|
250
|
+
/**
|
|
251
|
+
* Whether `element` has, for an attribute the XPath `selector` states (the
|
|
252
|
+
* text, description, label, name or value), something other than what it
|
|
253
|
+
* says: `@text='OK'` on an element reading "Delete". Resilio compares paths,
|
|
254
|
+
* in which a changed text weighs little, so a dialog's OK button that now
|
|
255
|
+
* reads Delete would otherwise be healed to. A stated attribute may belong to
|
|
256
|
+
* another element of the XPath (an ancestor, a sibling); Resilio then
|
|
257
|
+
* declines a heal it could have made, and the next tier decides.
|
|
258
|
+
*/
|
|
259
|
+
function contradictsSelector(element, strategy, selector) {
|
|
260
|
+
if (strategy !== 'xpath' || typeof selector !== 'string')
|
|
261
|
+
return false;
|
|
262
|
+
const valueOf = (name) => { var _a; return (_a = element.getAttribute(name)) !== null && _a !== void 0 ? _a : ''; };
|
|
263
|
+
const equals = new RegExp(`@(${STATED_ATTRIBUTES})\\s*=\\s*(['"])(.*?)\\2`, 'g');
|
|
264
|
+
for (const [, name, , value] of selector.matchAll(equals)) {
|
|
265
|
+
if (valueOf(name) !== value)
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
const contains = new RegExp(`contains\\(\\s*@(${STATED_ATTRIBUTES})\\s*,\\s*(['"])(.*?)\\2\\s*\\)`, 'g');
|
|
269
|
+
for (const [, name, , value] of selector.matchAll(contains)) {
|
|
270
|
+
if (!valueOf(name).includes(value))
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.onIdentityChanged = onIdentityChanged;
|
|
16
|
+
exports.identityChanged = identityChanged;
|
|
17
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
18
|
+
const listeners = new Set();
|
|
19
|
+
/** Returns the function that stops listening. */
|
|
20
|
+
function onIdentityChanged(listener) {
|
|
21
|
+
listeners.add(listener);
|
|
22
|
+
return () => {
|
|
23
|
+
listeners.delete(listener);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Never rejects: a listener's failure is logged and the write it follows stands. */
|
|
27
|
+
function identityChanged(userId) {
|
|
28
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
29
|
+
if (!userId)
|
|
30
|
+
return;
|
|
31
|
+
yield Promise.all(Array.from(listeners, (listener) => __awaiter(this, void 0, void 0, function* () {
|
|
32
|
+
var _a;
|
|
33
|
+
try {
|
|
34
|
+
yield listener(userId);
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
logger_1.default.warn(`[Identity] A change to user ${userId} was not applied to a live connection: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
38
|
+
}
|
|
39
|
+
})));
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
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;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
9
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
10
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
11
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
12
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
13
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
14
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
18
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
|
+
};
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.SessionDeviceLogs = exports.WRITE_BATCH = exports.FLUSH_INTERVAL_MS = void 0;
|
|
22
|
+
const typedi_1 = require("typedi");
|
|
23
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
24
|
+
const prisma_1 = require("../../prisma");
|
|
25
|
+
const PluginContext_1 = require("../../PluginContext");
|
|
26
|
+
const localDeviceHosts_1 = require("../../device-managers/localDeviceHosts");
|
|
27
|
+
const LogcatStreamService_1 = require("../../device-managers/android/LogcatStreamService");
|
|
28
|
+
const adbForPhone_1 = require("../network/adbForPhone");
|
|
29
|
+
const deviceLogBook_1 = require("./deviceLogBook");
|
|
30
|
+
const iosDriverLog_1 = require("./iosDriverLog");
|
|
31
|
+
/** How often a session's new lines are written. */
|
|
32
|
+
exports.FLUSH_INTERVAL_MS = 5000;
|
|
33
|
+
/** Lines per write, and how many waiting lines start one before the interval. */
|
|
34
|
+
exports.WRITE_BATCH = 500;
|
|
35
|
+
/** The phone's `date`, read once a session; one that hangs this long is unknown. */
|
|
36
|
+
const CLOCK_TIMEOUT_MS = 5000;
|
|
37
|
+
/** Waits before opening the phone's log stream again: doubling from the first, up to the last. */
|
|
38
|
+
const RETRY_FIRST_MS = 1000;
|
|
39
|
+
const RETRY_MAX_MS = 30000;
|
|
40
|
+
/**
|
|
41
|
+
* The session page's Device logs for a session on this server's own Android
|
|
42
|
+
* phone, iPhone or simulator: its lines from start to end, each once, with
|
|
43
|
+
* its own time and level, up to a limit (DeviceLogBook).
|
|
44
|
+
*
|
|
45
|
+
* An Android session listens to the phone's log stream (LogcatStreamService, one
|
|
46
|
+
* `adb logcat` per phone, which the device page's Logs viewer may share) from
|
|
47
|
+
* EventManager's start, once the session's row exists, to its stop, which
|
|
48
|
+
* every ending reaches. While it listens, the stream has a viewer, so its idle
|
|
49
|
+
* stop leaves it running; afterwards it stops 30 s after its last viewer, as
|
|
50
|
+
* ever. The session never stops or restarts it. If the stream ends (the phone
|
|
51
|
+
* restarted, or was unplugged), the session opens it again and notes the gap.
|
|
52
|
+
*
|
|
53
|
+
* An iPhone or simulator session listens to the XCUITest driver's own log
|
|
54
|
+
* of the device (iosDriverLog.ts), which the driver captures from early in
|
|
55
|
+
* the create to the session's end, after reading the lines it already holds.
|
|
56
|
+
* Neither takes lines from the driver, so a test's own `getLog('syslog')` is
|
|
57
|
+
* unchanged. It isn't the device page's Logs viewer stream (`go-ios
|
|
58
|
+
* ostrace`): that one has no simulators, needs the phone's go-ios tunnel on
|
|
59
|
+
* iOS 17+, serves one reader per phone and carries ten times the lines.
|
|
60
|
+
*
|
|
61
|
+
* Until 2.14 each recorded command dumped the last 500 lines (`logcat -d`),
|
|
62
|
+
* kept the last 100 and skipped as many as the previous dump had given, so
|
|
63
|
+
* after the first command nothing more was ever saved, each line stamped with
|
|
64
|
+
* the time it was saved. iPhones lost lines the same way, and simulators
|
|
65
|
+
* saved none.
|
|
66
|
+
*/
|
|
67
|
+
let SessionDeviceLogs = class SessionDeviceLogs {
|
|
68
|
+
constructor() {
|
|
69
|
+
this.log = logger_1.default.scope('DeviceLogs');
|
|
70
|
+
this.running = new Map();
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Whether a session on this device is recorded: an Android phone or
|
|
74
|
+
* emulator, an iPhone or a simulator, that this server drives.
|
|
75
|
+
*/
|
|
76
|
+
appliesTo(device) {
|
|
77
|
+
var _a;
|
|
78
|
+
if (!device || device.cloud)
|
|
79
|
+
return false;
|
|
80
|
+
if (!['android', 'ios', 'tvos'].includes(String((_a = device.platform) !== null && _a !== void 0 ? _a : '').toLowerCase())) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
// A node's phone isn't reachable from here (its driver and its adb are
|
|
84
|
+
// the node's); the node has the session, and no row for it.
|
|
85
|
+
const ctx = this.context();
|
|
86
|
+
return (0, localDeviceHosts_1.isOwnDevice)((0, localDeviceHosts_1.localDeviceHosts)(ctx.pluginArgs, ctx.port), ctx.nodeId, device);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Starts recording the session's device log. Resolves once it listens to
|
|
90
|
+
* the phone's stream (or is waiting to try again); callers needn't wait.
|
|
91
|
+
*/
|
|
92
|
+
start({ sessionId, device, since, driverLog, appUnderTest }) {
|
|
93
|
+
var _a, _b;
|
|
94
|
+
if (this.running.has(sessionId) || !this.appliesTo(device))
|
|
95
|
+
return Promise.resolve();
|
|
96
|
+
const android = String(device.platform).toLowerCase() === 'android';
|
|
97
|
+
if (!android && !(0, iosDriverLog_1.isDriverLog)(driverLog)) {
|
|
98
|
+
this.log.info(`[${sessionId}] No device log for ${device.udid}: the driver isn't capturing one ` +
|
|
99
|
+
'(appium:skipLogCapture?)');
|
|
100
|
+
return Promise.resolve();
|
|
101
|
+
}
|
|
102
|
+
const entry = {
|
|
103
|
+
udid: device.udid,
|
|
104
|
+
buffer: [],
|
|
105
|
+
flushTimer: setInterval(() => void this.write(sessionId, entry), this.flushIntervalMs()),
|
|
106
|
+
writing: Promise.resolve(),
|
|
107
|
+
lastCreatedAt: 0,
|
|
108
|
+
attempts: 0,
|
|
109
|
+
stopped: false,
|
|
110
|
+
};
|
|
111
|
+
(_b = (_a = entry.flushTimer).unref) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
112
|
+
this.running.set(sessionId, entry);
|
|
113
|
+
this.log.info(`[${sessionId}] Recording the device log of ${device.udid}`);
|
|
114
|
+
const opened = android
|
|
115
|
+
? this.open(sessionId, entry, since !== null && since !== void 0 ? since : Date.now())
|
|
116
|
+
: Promise.resolve(this.listenToDriver(sessionId, entry, since !== null && since !== void 0 ? since : Date.now(), driverLog, appUnderTest));
|
|
117
|
+
return opened.catch((err) => { var _a; return this.log.warn(`[${sessionId}] Device log not recorded: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`); });
|
|
118
|
+
}
|
|
119
|
+
/** Stops listening and writes what is left. Idempotent. */
|
|
120
|
+
stop(sessionId) {
|
|
121
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
122
|
+
var _a;
|
|
123
|
+
const entry = this.running.get(sessionId);
|
|
124
|
+
if (!entry)
|
|
125
|
+
return;
|
|
126
|
+
this.running.delete(sessionId);
|
|
127
|
+
entry.stopped = true;
|
|
128
|
+
clearInterval(entry.flushTimer);
|
|
129
|
+
if (entry.retryTimer)
|
|
130
|
+
clearTimeout(entry.retryTimer);
|
|
131
|
+
(_a = entry.remove) === null || _a === void 0 ? void 0 : _a.call(entry);
|
|
132
|
+
entry.remove = undefined;
|
|
133
|
+
if (entry.book)
|
|
134
|
+
entry.buffer.push(...entry.book.finish(new Date()));
|
|
135
|
+
yield this.write(sessionId, entry);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/** Whether the session is recorded here. */
|
|
139
|
+
isRecording(sessionId) {
|
|
140
|
+
return this.running.has(sessionId);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* A session that turned its device log off (`xe:save_device_logs`): its
|
|
144
|
+
* Device logs tab says so, rather than look empty for no reason.
|
|
145
|
+
*/
|
|
146
|
+
noteOff(sessionId) {
|
|
147
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
148
|
+
var _a;
|
|
149
|
+
try {
|
|
150
|
+
yield this.writeLines(sessionId, [
|
|
151
|
+
{
|
|
152
|
+
message: "Xenon: This session's device log wasn't kept: the test turned it off.",
|
|
153
|
+
timestamp: new Date(),
|
|
154
|
+
},
|
|
155
|
+
], Date.now());
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
this.log.debug(`[${sessionId}] Couldn't note that the device log is off: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
open(sessionId, entry, since) {
|
|
163
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
164
|
+
var _a;
|
|
165
|
+
let clock = null;
|
|
166
|
+
try {
|
|
167
|
+
clock = yield this.readClock(entry.udid);
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
this.log.debug(`[${sessionId}] Can't read the clock of ${entry.udid}: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
171
|
+
}
|
|
172
|
+
if (!clock) {
|
|
173
|
+
this.log.warn(`[${sessionId}] The clock of ${entry.udid} is unknown; its log lines are read as this server's time`);
|
|
174
|
+
}
|
|
175
|
+
if (entry.stopped)
|
|
176
|
+
return;
|
|
177
|
+
entry.book = new deviceLogBook_1.DeviceLogBook({ since, clock: clock !== null && clock !== void 0 ? clock : deviceLogBook_1.UNKNOWN_CLOCK });
|
|
178
|
+
yield this.listen(sessionId, entry);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
listen(sessionId, entry) {
|
|
182
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
183
|
+
var _a;
|
|
184
|
+
entry.retryTimer = undefined;
|
|
185
|
+
if (entry.stopped)
|
|
186
|
+
return;
|
|
187
|
+
let mux;
|
|
188
|
+
try {
|
|
189
|
+
mux = yield this.streams().start(entry.udid);
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
if (entry.attempts === 0) {
|
|
193
|
+
this.log.warn(`[${sessionId}] Can't open the device log of ${entry.udid}: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}. Trying again.`);
|
|
194
|
+
}
|
|
195
|
+
this.retry(sessionId, entry);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
// Stopped while the stream started: leave it to its own idle stop.
|
|
199
|
+
if (entry.stopped)
|
|
200
|
+
return;
|
|
201
|
+
const book = entry.book;
|
|
202
|
+
let delivered = false;
|
|
203
|
+
const remove = mux.addClient((rec) => {
|
|
204
|
+
if (rec.synthetic)
|
|
205
|
+
return;
|
|
206
|
+
if (!delivered) {
|
|
207
|
+
delivered = true;
|
|
208
|
+
entry.attempts = 0;
|
|
209
|
+
}
|
|
210
|
+
const rows = book.add(rec);
|
|
211
|
+
if (rows.length === 0)
|
|
212
|
+
return;
|
|
213
|
+
entry.buffer.push(...rows);
|
|
214
|
+
if (entry.buffer.length >= exports.WRITE_BATCH)
|
|
215
|
+
void this.write(sessionId, entry);
|
|
216
|
+
},
|
|
217
|
+
// Never refused: what the session keeps is bounded by its book.
|
|
218
|
+
() => true, () => {
|
|
219
|
+
if (entry.remove === remove)
|
|
220
|
+
entry.remove = undefined;
|
|
221
|
+
if (entry.stopped)
|
|
222
|
+
return;
|
|
223
|
+
const note = book.interrupted(new Date());
|
|
224
|
+
// A stream that delivered nothing (a phone that's gone) isn't noted
|
|
225
|
+
// again on every try.
|
|
226
|
+
if (delivered) {
|
|
227
|
+
entry.buffer.push(...note);
|
|
228
|
+
this.log.info(`[${sessionId}] The device log of ${entry.udid} ended; opening it again`);
|
|
229
|
+
}
|
|
230
|
+
this.retry(sessionId, entry);
|
|
231
|
+
});
|
|
232
|
+
entry.remove = remove;
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* An iPhone or simulator: the lines the driver already holds, then each one
|
|
237
|
+
* as it arrives, until the session stops. Synchronous, so no line can arrive
|
|
238
|
+
* between the two. Stamped with the time each reached this server, a moment
|
|
239
|
+
* after the device logged it.
|
|
240
|
+
*/
|
|
241
|
+
listenToDriver(sessionId, entry, since, driverLog, appUnderTest) {
|
|
242
|
+
var _a;
|
|
243
|
+
if (!(0, iosDriverLog_1.isDriverLog)(driverLog))
|
|
244
|
+
return;
|
|
245
|
+
const book = new deviceLogBook_1.DeviceLogBook({ since, clock: deviceLogBook_1.UNKNOWN_CLOCK, format: iosDriverLog_1.iosLineText });
|
|
246
|
+
entry.book = book;
|
|
247
|
+
const appLines = appUnderTest ? new iosDriverLog_1.IosAppLines(appUnderTest) : undefined;
|
|
248
|
+
if (appLines) {
|
|
249
|
+
entry.buffer.push({
|
|
250
|
+
message: `Xenon: Kept here: what the app under test (${appUnderTest}) logs itself, ` +
|
|
251
|
+
'every error inside it, what the phone says about its launch, state, crashes ' +
|
|
252
|
+
"and end, and faults. The rest of the phone's log is left out.",
|
|
253
|
+
timestamp: new Date(since),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
const take = (line) => {
|
|
257
|
+
const rec = (0, iosDriverLog_1.recordFromDriverLog)(line);
|
|
258
|
+
if (!rec)
|
|
259
|
+
return;
|
|
260
|
+
if (appLines && !appLines.keeps(rec.message, rec.level))
|
|
261
|
+
return;
|
|
262
|
+
const rows = book.add(rec);
|
|
263
|
+
if (rows.length === 0)
|
|
264
|
+
return;
|
|
265
|
+
entry.buffer.push(...rows);
|
|
266
|
+
if (entry.buffer.length >= exports.WRITE_BATCH)
|
|
267
|
+
void this.write(sessionId, entry);
|
|
268
|
+
};
|
|
269
|
+
// The create's lines first teach the filter the app's process, so its
|
|
270
|
+
// first lines aren't lost to an announcement that comes after them.
|
|
271
|
+
const buffered = (0, iosDriverLog_1.bufferedDriverLog)(driverLog);
|
|
272
|
+
for (const line of buffered)
|
|
273
|
+
appLines === null || appLines === void 0 ? void 0 : appLines.learn((_a = line.message) !== null && _a !== void 0 ? _a : '');
|
|
274
|
+
for (const line of buffered)
|
|
275
|
+
take(line);
|
|
276
|
+
driverLog.on('output', take);
|
|
277
|
+
entry.remove = () => driverLog.removeListener('output', take);
|
|
278
|
+
}
|
|
279
|
+
retry(sessionId, entry) {
|
|
280
|
+
var _a, _b;
|
|
281
|
+
if (entry.stopped || entry.retryTimer)
|
|
282
|
+
return;
|
|
283
|
+
entry.attempts += 1;
|
|
284
|
+
const delay = Math.min(this.retryFirstMs() * 2 ** (entry.attempts - 1), RETRY_MAX_MS);
|
|
285
|
+
entry.retryTimer = setTimeout(() => void this.listen(sessionId, entry), delay);
|
|
286
|
+
(_b = (_a = entry.retryTimer).unref) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Writes what is waiting, WRITE_BATCH rows at a time. On a failure the rows
|
|
290
|
+
* wait for the next write: the book bounds how many there can be.
|
|
291
|
+
*/
|
|
292
|
+
write(sessionId, entry) {
|
|
293
|
+
entry.writing = entry.writing.then(() => __awaiter(this, void 0, void 0, function* () {
|
|
294
|
+
var _a;
|
|
295
|
+
while (entry.buffer.length > 0) {
|
|
296
|
+
const batch = entry.buffer.splice(0, exports.WRITE_BATCH);
|
|
297
|
+
// The session's rows are read in `createdAt` order, so each is a
|
|
298
|
+
// millisecond after the one before: the lines of one moment (a stack
|
|
299
|
+
// trace's) keep their order whatever the database.
|
|
300
|
+
const first = Math.max(Date.now(), entry.lastCreatedAt + 1);
|
|
301
|
+
try {
|
|
302
|
+
yield this.writeLines(sessionId, batch, first);
|
|
303
|
+
entry.lastCreatedAt = first + batch.length - 1;
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
entry.buffer.unshift(...batch);
|
|
307
|
+
this.log.warn(`[${sessionId}] Writing ${batch.length} device log lines failed: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}));
|
|
312
|
+
return entry.writing;
|
|
313
|
+
}
|
|
314
|
+
// ---------------------------------------------------------------------
|
|
315
|
+
// Seams. Overridden by tests; the defaults are the real thing.
|
|
316
|
+
// ---------------------------------------------------------------------
|
|
317
|
+
context() {
|
|
318
|
+
return typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
319
|
+
}
|
|
320
|
+
streams() {
|
|
321
|
+
return typedi_1.Container.get(LogcatStreamService_1.LogcatStreamService);
|
|
322
|
+
}
|
|
323
|
+
flushIntervalMs() {
|
|
324
|
+
return exports.FLUSH_INTERVAL_MS;
|
|
325
|
+
}
|
|
326
|
+
retryFirstMs() {
|
|
327
|
+
return RETRY_FIRST_MS;
|
|
328
|
+
}
|
|
329
|
+
/** The phone's clock against this server's, or null if its answer isn't one. */
|
|
330
|
+
readClock(udid) {
|
|
331
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
332
|
+
const adb = yield (0, adbForPhone_1.adbForPhone)(udid);
|
|
333
|
+
const asked = Date.now();
|
|
334
|
+
// One word: the phone's shell would take a second one as a time to set.
|
|
335
|
+
const out = yield adb.adbExec(['-s', udid, 'shell', deviceLogBook_1.DEVICE_CLOCK_COMMAND], {
|
|
336
|
+
timeout: CLOCK_TIMEOUT_MS,
|
|
337
|
+
});
|
|
338
|
+
return (0, deviceLogBook_1.parseDeviceClock)(String(out !== null && out !== void 0 ? out : ''), (asked + Date.now()) / 2);
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
writeLines(sessionId, lines, firstCreatedAt) {
|
|
342
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
343
|
+
yield prisma_1.prisma.log.createMany({
|
|
344
|
+
data: lines.map((line, i) => ({
|
|
345
|
+
session_id: sessionId,
|
|
346
|
+
log_type: 'DEVICE',
|
|
347
|
+
message: line.message,
|
|
348
|
+
timestamp: line.timestamp,
|
|
349
|
+
createdAt: new Date(firstCreatedAt + i),
|
|
350
|
+
})),
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
exports.SessionDeviceLogs = SessionDeviceLogs;
|
|
356
|
+
exports.SessionDeviceLogs = SessionDeviceLogs = __decorate([
|
|
357
|
+
(0, typedi_1.Service)()
|
|
358
|
+
], SessionDeviceLogs);
|