@xenon-device-management/xenon 2.14.0 → 2.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/lib/package.json +5 -5
- package/lib/public/assets/{AnnotationOverlay-B_VNtKBH.js → AnnotationOverlay-Btq3dZRI.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-BwuaMjkX.js → ApiKeyGate-C9OoAgg_.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-DBC_xczB.js +2 -0
- package/lib/public/assets/{EmptyState-BSjhk_IV.js → EmptyState-DqWU2-vM.js} +1 -1
- package/lib/public/assets/{FieldGroup-Cb5QZsE1.js → FieldGroup-CveQ3xrA.js} +1 -1
- package/lib/public/assets/{FilterMenu-BAaSlWbW.js → FilterMenu-BfKDSfHz.js} +1 -1
- package/lib/public/assets/{Menu-JGGYrSQQ.js → Menu-CtdCybzA.js} +1 -1
- package/lib/public/assets/{Modal-CFCp3n9d.js → Modal-Bsvj1tm2.js} +1 -1
- package/lib/public/assets/{RecordingPage-CbJj_EBT.js → RecordingPage-CRofOiyx.js} +1 -1
- package/lib/public/assets/{RecordingsPage-BJeYnygR.js → RecordingsPage-jNQHb2IR.js} +1 -1
- package/lib/public/assets/{SegmentedControl-Bg42Atjy.js → SegmentedControl-C8HtTha-.js} +1 -1
- package/lib/public/assets/{SettingCard-Njz1pUIJ.js → SettingCard-D8K1LZNo.js} +1 -1
- package/lib/public/assets/{Table-BcTm5xV2.js → Table-zNTX92Pn.js} +1 -1
- package/lib/public/assets/{activity-DOVdsfMQ.js → activity-CJ8LwZq9.js} +1 -1
- package/lib/public/assets/ai-settings-CgxQZqrc.js +16 -0
- package/lib/public/assets/{api-keys-BKnNSTw9.js → api-keys-ECpo-zTo.js} +1 -1
- package/lib/public/assets/{apps-D0SC6uw6.js → apps-BjGymyf3.js} +1 -1
- package/lib/public/assets/{arrow-left-MMbTmgoE.js → arrow-left-BWfCbeHP.js} +1 -1
- package/lib/public/assets/{arrow-right-BMfnGmxW.js → arrow-right-DC7UGf_n.js} +1 -1
- package/lib/public/assets/{arrow-up-right-DuAftMWB.js → arrow-up-right-BOjwybDk.js} +1 -1
- package/lib/public/assets/{auth-shell-nZC2JNDO.js → auth-shell-CxMTo9aH.js} +2 -2
- package/lib/public/assets/{builds-page-hDCgTwkS.js → builds-page-DOCm4HkA.js} +1 -1
- package/lib/public/assets/{button-Dda4irn_.js → button-Cg08-EMT.js} +1 -1
- package/lib/public/assets/{calendar-BMr0Q62v.js → calendar-CA1ikLJs.js} +1 -1
- package/lib/public/assets/{check-CZ9iw9-e.js → check-BMTaH72W.js} +1 -1
- package/lib/public/assets/{chevron-right-BRYJJVC9.js → chevron-right-B2QOv9Nx.js} +1 -1
- package/lib/public/assets/{circle-check-CqAJjQ5O.js → circle-check-CYXBoUqT.js} +1 -1
- package/lib/public/assets/{circle-x-DlDJtnVz.js → circle-x-DTwnSAnl.js} +1 -1
- package/lib/public/assets/{clock-CpuDKysU.js → clock-C8_HViQP.js} +1 -1
- package/lib/public/assets/{copy-CAQydNFe.js → copy-Be7LNMXV.js} +1 -1
- package/lib/public/assets/derive-UnLRp4r3.js +1 -0
- package/lib/public/assets/device-explorer-BOqtfGXz.js +282 -0
- package/lib/public/assets/{download-C9XG6lLv.js → download-B56lwxWP.js} +1 -1
- package/lib/public/assets/forgot-password-CCU6bzLn.js +1 -0
- package/lib/public/assets/{h264Stream-VQBphYhg.js → h264Stream-C-OtmIuk.js} +6 -6
- package/lib/public/assets/index-B9KQ6LsY.js +15 -0
- package/lib/public/assets/{index-BbKEXUFH.js → index-BOxe43p1.js} +2 -2
- package/lib/public/assets/{index-DJGc1M2f.js → index-CfxsUslp.js} +1 -1
- package/lib/public/assets/{input-Cm0tmSoc.js → input-CZHFjbSg.js} +1 -1
- package/lib/public/assets/{line-chart-D2DyJCml.js → line-chart-Gl_Dz3X_.js} +1 -1
- package/lib/public/assets/{list-checks-UWf1GY99.js → list-checks-CCgjeyGY.js} +1 -1
- package/lib/public/assets/{lock-CCJv9m_I.js → lock-CkOwgDcx.js} +1 -1
- package/lib/public/assets/{login-f8vOVW-U.js → login-D5cc4w7M.js} +1 -1
- package/lib/public/assets/{maintenance-settings-C1PrkuAH.js → maintenance-settings-t7qr3fvX.js} +1 -1
- package/lib/public/assets/{monitor-B2jDgwm7.js → monitor-DEJixKQO.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-DLI1RtzU.js → mouse-pointer-2-CtOrk4M8.js} +1 -1
- package/lib/public/assets/{network-ebT86qoi.js → network-Dii9qdvk.js} +1 -1
- package/lib/public/assets/{overview-C0aykVrd.js → overview-rolAACrO.js} +1 -1
- package/lib/public/assets/{page-header-BjzgWz-T.js → page-header-COQZxJfR.js} +1 -1
- package/lib/public/assets/{play-DLG9t8FU.js → play-DrN1Se_R.js} +1 -1
- package/lib/public/assets/{plus-nBFSMu_X.js → plus-5aq-9uRx.js} +1 -1
- package/lib/public/assets/{profile-page-CSl0UYlv.js → profile-page-6i5kY6kF.js} +1 -1
- package/lib/public/assets/{reset-password-LeOuHqmB.js → reset-password-sp2IBrc7.js} +1 -1
- package/lib/public/assets/runbook-page-DJDjeopU.js +253 -0
- package/lib/public/assets/{select-88jHcl-Z.js → select-Bpyn7rWO.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BbXKJjZM.js → selector-detail-redirect-BH74vmKv.js} +1 -1
- package/lib/public/assets/{selector-health-page-B4rA1Xkv.js → selector-health-page-Cw_g218H.js} +1 -1
- package/lib/public/assets/session-detail-page-AZJE6XXE.js +4 -0
- package/lib/public/assets/{settings-GF6ZMSQ6.js → settings-C3uyGhyc.js} +1 -1
- package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
- package/lib/public/assets/{stat-tile-BV98e_Sv.js → stat-tile-xVqAKIlI.js} +1 -1
- package/lib/public/assets/{tablet-BiAsnFDj.js → tablet-BxUScAeO.js} +1 -1
- package/lib/public/assets/{teams-B4-jrr-1.js → teams-B5ljGkY2.js} +1 -1
- package/lib/public/assets/{trash-2-C5RSuX0e.js → trash-2-B701i8sy.js} +1 -1
- package/lib/public/assets/{upload-BG7_rMXK.js → upload-C2gr44RS.js} +1 -1
- package/lib/public/assets/{use-builds-data-J-riQYyC.js → use-builds-data-NeQOFEoy.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-C8-eH-UG.js → use-password-reset-mode-DEzJq5ef.js} +1 -1
- package/lib/public/assets/{users-CFba5TSy.js → users-D_vv_yz4.js} +1 -1
- package/lib/public/assets/{users-Cp0beJm3.js → users-MY6eNe6s.js} +1 -1
- package/lib/public/assets/{video-off-DDsEISjV.js → video-off-OK1lNZQn.js} +1 -1
- package/lib/public/assets/{webhook-settings-BGu32vSB.js → webhook-settings-BU-C9lBf.js} +1 -1
- package/lib/public/assets/{zap-l4VaXzq8.js → zap-DO9ry1iz.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +5 -5
- 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 +10 -3
- package/lib/src/app/routers/apikeys.js +14 -2
- package/lib/src/app/routers/auth.js +29 -5
- package/lib/src/app/routers/control.js +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 +10 -5
- package/lib/src/app/swagger.js +2 -0
- package/lib/src/app/ws/h264StreamWs.js +10 -1
- package/lib/src/config.js +1 -0
- package/lib/src/dashboard/commands.js +5 -5
- package/lib/src/dashboard/event-manager.js +49 -61
- package/lib/src/dashboard/services/failure-analysis-service.js +136 -45
- 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/android/AndroidH264StreamService.js +69 -20
- package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
- package/lib/src/device-utils.js +20 -2
- package/lib/src/gateway/defaultGateway.js +1 -1
- package/lib/src/gateway/forwardToNode.js +2 -0
- package/lib/src/gateway/healReport.js +87 -0
- package/lib/src/gateway/sessionGateway.js +12 -2
- package/lib/src/generated/client/edge.js +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/interceptors/CommandInterceptor.js +243 -181
- 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 +8 -3
- package/lib/src/services/AICommandService.js +9 -2
- package/lib/src/services/AIService.js +192 -42
- package/lib/src/services/EmailService.js +29 -0
- package/lib/src/services/OrphanSweeper.js +2 -0
- package/lib/src/services/SelectorVerificationJob.js +15 -4
- package/lib/src/services/ServerManager.js +11 -1
- package/lib/src/services/SessionLifecycleService.js +86 -26
- package/lib/src/services/SocketServer.js +19 -3
- package/lib/src/services/bug-report/video-slice.js +4 -1
- package/lib/src/services/device-access/leaseOverride.js +7 -5
- package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
- package/lib/src/services/healing/HealEtalonService.js +3 -2
- package/lib/src/services/healing/HealingOrchestrator.js +55 -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/logcat/SessionDeviceLogs.js +271 -0
- package/lib/src/services/logcat/deviceLogBook.js +163 -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/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/mintedLifetime.js +51 -0
- package/lib/src/sessions/SessionManager.js +8 -2
- package/lib/src/sessions/sessionMemory.js +25 -0
- package/lib/test/helpers/fake-ai-provider.js +188 -0
- package/lib/test/helpers/loki-stores.js +33 -0
- package/lib/test/helpers/page-sources.js +164 -0
- package/lib/test/helpers/scratch-database.js +57 -13
- package/lib/test/integration/audit-ingest.spec.js +10 -10
- package/lib/test/integration/auth-flow.spec.js +9 -14
- package/lib/test/integration/auth-rate-limit.spec.js +6 -3
- package/lib/test/integration/bug-report-route.spec.js +22 -2
- package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
- package/lib/test/integration/forgot-password.spec.js +17 -18
- package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
- package/lib/test/integration/migration.spec.js +6 -1
- package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
- package/lib/test/integration/ports-router.spec.js +4 -4
- package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
- package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
- package/lib/test/integration/profile-tokens.spec.js +10 -16
- package/lib/test/integration/recordings-router.spec.js +16 -16
- package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
- package/lib/test/integration/role-matrix.spec.js +4 -9
- package/lib/test/integration/sdk-leases-router.spec.js +9 -9
- package/lib/test/integration/sdk-version.spec.js +2 -2
- package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
- package/lib/test/integration/team-membership-migration.spec.js +73 -12
- package/lib/test/integration/team-visibility-apps.spec.js +15 -22
- package/lib/test/integration/team-visibility-control.spec.js +96 -69
- package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
- package/lib/test/integration/team-visibility-grid.spec.js +16 -15
- package/lib/test/integration/team-visibility-leases.spec.js +8 -11
- package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
- package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
- package/lib/test/integration/users-flow.spec.js +7 -12
- package/lib/test/integration/users-lockout.spec.js +5 -3
- package/lib/test/unit/EmailService.test.js +66 -0
- package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
- package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
- package/lib/test/unit/ai-engine-settings.spec.js +338 -0
- package/lib/test/unit/ai-provider-switch.spec.js +130 -0
- package/lib/test/unit/ai-rate-limit.spec.js +126 -0
- package/lib/test/unit/android-h264-viewers.spec.js +272 -0
- package/lib/test/unit/android-preview-hold.spec.js +3 -2
- package/lib/test/unit/apikeys-router.spec.js +84 -0
- package/lib/test/unit/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/dashboard-events-device-logs.spec.js +207 -0
- package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
- package/lib/test/unit/device-log-book.spec.js +180 -0
- package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
- package/lib/test/unit/event-log-session-data.spec.js +244 -0
- package/lib/test/unit/failure-analysis.spec.js +182 -0
- package/lib/test/unit/failure-categories.spec.js +128 -0
- package/lib/test/unit/fingerprint-identity.spec.js +433 -0
- package/lib/test/unit/h264-multiplexer.spec.js +46 -0
- package/lib/test/unit/heal-report.spec.js +71 -0
- package/lib/test/unit/healing-instrumentation.spec.js +6 -1
- package/lib/test/unit/healing-tier-gate.spec.js +73 -29
- package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +171 -0
- package/lib/test/unit/interceptor-visual-elements.spec.js +192 -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/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/package-engines.spec.js +27 -0
- package/lib/test/unit/passwordResetMode.test.js +10 -2
- package/lib/test/unit/profile-router.test.js +122 -1
- 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/selector-learning-every-session.spec.js +179 -0
- package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
- package/lib/test/unit/selector-verification-job.spec.js +13 -13
- package/lib/test/unit/self-healing-boot.spec.js +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.spec.js +347 -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/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/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 +5 -5
- package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
- package/prisma/schema.prisma +3 -0
- package/schema.json +5 -5
- 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/runbook-page-CMMeQml9.js +0 -124
- package/lib/public/assets/session-detail-page-COL_RZxw.js +0 -4
- /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
|
@@ -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
|
@@ -648,7 +648,7 @@ function unblockCandidateDevices() {
|
|
|
648
648
|
}
|
|
649
649
|
function releaseBlockedDevices(newCommandTimeout) {
|
|
650
650
|
return __awaiter(this, void 0, void 0, function* () {
|
|
651
|
-
var _a, _b;
|
|
651
|
+
var _a, _b, _c;
|
|
652
652
|
const busyDevices = yield unblockCandidateDevices();
|
|
653
653
|
// A phone an SDK lease holds is the lease's to time out (its heartbeats and
|
|
654
654
|
// expiry, LeaseOrphanSweeper), not idle until a session runs on it. A lease
|
|
@@ -698,6 +698,7 @@ function releaseBlockedDevices(newCommandTimeout) {
|
|
|
698
698
|
logger_1.default.info(`Unblocking device ${device.udid} at host ${device.host} because it has been idle for ${timeSinceLastCmdExecuted} seconds`);
|
|
699
699
|
// Before anything below can release the phone (onSessionStopped too).
|
|
700
700
|
yield restoreIdleSessionNetwork((_a = device.claimSessionId) !== null && _a !== void 0 ? _a : device.session_id);
|
|
701
|
+
yield forgetIdleSessionMemory((_b = device.claimSessionId) !== null && _b !== void 0 ? _b : device.session_id);
|
|
701
702
|
// Principal Protection: If this device has an active dashboard session, stop it properly
|
|
702
703
|
if (device.session_id) {
|
|
703
704
|
const sessionId = device.session_id;
|
|
@@ -713,7 +714,7 @@ function releaseBlockedDevices(newCommandTimeout) {
|
|
|
713
714
|
}
|
|
714
715
|
// Keyed on the session read here, so a release that loses the race to
|
|
715
716
|
// the session's own end (and the phone's next claim) frees nothing.
|
|
716
|
-
const claimant = (
|
|
717
|
+
const claimant = (_c = device.claimSessionId) !== null && _c !== void 0 ? _c : device.session_id;
|
|
717
718
|
yield (claimant
|
|
718
719
|
? (0, device_service_1.releaseSessionDevice)(device.udid, device.host, claimant)
|
|
719
720
|
: (0, device_service_1.unblockDevice)(device.udid, device.host)).catch((err) => logger_1.default.error(`Unable to release ${device.udid}: ${err}`));
|
|
@@ -739,6 +740,23 @@ function restoreIdleSessionNetwork(sessionId) {
|
|
|
739
740
|
}
|
|
740
741
|
});
|
|
741
742
|
}
|
|
743
|
+
/**
|
|
744
|
+
* The idle session's virtual elements and autowait settings
|
|
745
|
+
* (sessionMemory.ts). Imported lazily, as above. Never throws.
|
|
746
|
+
*/
|
|
747
|
+
function forgetIdleSessionMemory(sessionId) {
|
|
748
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
749
|
+
if (!sessionId)
|
|
750
|
+
return;
|
|
751
|
+
try {
|
|
752
|
+
const { forgetSessionMemory } = yield Promise.resolve().then(() => __importStar(require('./sessions/sessionMemory')));
|
|
753
|
+
forgetSessionMemory(sessionId);
|
|
754
|
+
}
|
|
755
|
+
catch (err) {
|
|
756
|
+
logger_1.default.warn(`Could not forget idle session ${sessionId}'s elements: ${err}`);
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
}
|
|
742
760
|
function setupCronReleaseBlockedDevices(intervalMs, newCommandTimeoutSec) {
|
|
743
761
|
return __awaiter(this, void 0, void 0, function* () {
|
|
744
762
|
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) {
|
|
@@ -15,6 +15,7 @@ const https_proxy_agent_1 = require("https-proxy-agent");
|
|
|
15
15
|
const http_proxy_agent_1 = require("http-proxy-agent");
|
|
16
16
|
const internalCall_1 = require("./internalCall");
|
|
17
17
|
const hubSessionToken_1 = require("./hubSessionToken");
|
|
18
|
+
const healReport_1 = require("./healReport");
|
|
18
19
|
/**
|
|
19
20
|
* Sending a session command on to the node that runs the session, and handing
|
|
20
21
|
* the node's answer back to the client unchanged.
|
|
@@ -44,6 +45,7 @@ const NEVER_FORWARDED = new Set([
|
|
|
44
45
|
'x-xenon-token',
|
|
45
46
|
internalCall_1.INTERNAL_CALL_HEADER,
|
|
46
47
|
hubSessionToken_1.HUB_TOKEN_HEADER,
|
|
48
|
+
healReport_1.HEAL_REPORT_HEADER,
|
|
47
49
|
]);
|
|
48
50
|
function forwardedRequestHeaders(incoming) {
|
|
49
51
|
const out = {};
|
|
@@ -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
|
+
}
|
|
@@ -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
|
});
|