@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
|
@@ -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,271 @@
|
|
|
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
|
+
/** How often a session's new lines are written. */
|
|
31
|
+
exports.FLUSH_INTERVAL_MS = 5000;
|
|
32
|
+
/** Lines per write, and how many waiting lines start one before the interval. */
|
|
33
|
+
exports.WRITE_BATCH = 500;
|
|
34
|
+
/** The phone's `date`, read once a session; one that hangs this long is unknown. */
|
|
35
|
+
const CLOCK_TIMEOUT_MS = 5000;
|
|
36
|
+
/** Waits before opening the phone's log stream again: doubling from the first, up to the last. */
|
|
37
|
+
const RETRY_FIRST_MS = 1000;
|
|
38
|
+
const RETRY_MAX_MS = 30000;
|
|
39
|
+
/**
|
|
40
|
+
* The session page's Device logs for a session on this server's own Android
|
|
41
|
+
* phone: its lines from start to end, each once, with its own time and level,
|
|
42
|
+
* up to a limit (DeviceLogBook).
|
|
43
|
+
*
|
|
44
|
+
* The session listens to the phone's log stream (LogcatStreamService, one
|
|
45
|
+
* `adb logcat` per phone, which the device page's Logs viewer may share) from
|
|
46
|
+
* EventManager's start, once the session's row exists, to its stop, which
|
|
47
|
+
* every ending reaches. While it listens, the stream has a viewer, so its idle
|
|
48
|
+
* stop leaves it running; afterwards it stops 30 s after its last viewer, as
|
|
49
|
+
* ever. The session never stops or restarts it. If the stream ends (the phone
|
|
50
|
+
* restarted, or was unplugged), the session opens it again and notes the gap.
|
|
51
|
+
*
|
|
52
|
+
* Until 2.14 each recorded command dumped the last 500 lines (`logcat -d`),
|
|
53
|
+
* kept the last 100 and skipped as many as the previous dump had given, so
|
|
54
|
+
* after the first command nothing more was ever saved, each line stamped with
|
|
55
|
+
* the time it was saved.
|
|
56
|
+
*/
|
|
57
|
+
let SessionDeviceLogs = class SessionDeviceLogs {
|
|
58
|
+
constructor() {
|
|
59
|
+
this.log = logger_1.default.scope('DeviceLogs');
|
|
60
|
+
this.running = new Map();
|
|
61
|
+
}
|
|
62
|
+
/** Whether a session on this phone is recorded: an Android phone (or emulator) this server drives. */
|
|
63
|
+
appliesTo(device) {
|
|
64
|
+
var _a;
|
|
65
|
+
if (!device || device.cloud)
|
|
66
|
+
return false;
|
|
67
|
+
if (String((_a = device.platform) !== null && _a !== void 0 ? _a : '').toLowerCase() !== 'android')
|
|
68
|
+
return false;
|
|
69
|
+
// A node's phone isn't reachable with this server's adb; the node has
|
|
70
|
+
// the session, and no row for it.
|
|
71
|
+
const ctx = this.context();
|
|
72
|
+
return (0, localDeviceHosts_1.isOwnDevice)((0, localDeviceHosts_1.localDeviceHosts)(ctx.pluginArgs, ctx.port), ctx.nodeId, device);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Starts recording the session's device log. Resolves once it listens to
|
|
76
|
+
* the phone's stream (or is waiting to try again); callers needn't wait.
|
|
77
|
+
*/
|
|
78
|
+
start({ sessionId, device, since }) {
|
|
79
|
+
var _a, _b;
|
|
80
|
+
if (this.running.has(sessionId) || !this.appliesTo(device))
|
|
81
|
+
return Promise.resolve();
|
|
82
|
+
const entry = {
|
|
83
|
+
udid: device.udid,
|
|
84
|
+
buffer: [],
|
|
85
|
+
flushTimer: setInterval(() => void this.write(sessionId, entry), this.flushIntervalMs()),
|
|
86
|
+
writing: Promise.resolve(),
|
|
87
|
+
lastCreatedAt: 0,
|
|
88
|
+
attempts: 0,
|
|
89
|
+
stopped: false,
|
|
90
|
+
};
|
|
91
|
+
(_b = (_a = entry.flushTimer).unref) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
92
|
+
this.running.set(sessionId, entry);
|
|
93
|
+
this.log.info(`[${sessionId}] Recording the device log of ${device.udid}`);
|
|
94
|
+
return this.open(sessionId, entry, since !== null && since !== void 0 ? since : Date.now()).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}`); });
|
|
95
|
+
}
|
|
96
|
+
/** Stops listening and writes what is left. Idempotent. */
|
|
97
|
+
stop(sessionId) {
|
|
98
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
99
|
+
var _a;
|
|
100
|
+
const entry = this.running.get(sessionId);
|
|
101
|
+
if (!entry)
|
|
102
|
+
return;
|
|
103
|
+
this.running.delete(sessionId);
|
|
104
|
+
entry.stopped = true;
|
|
105
|
+
clearInterval(entry.flushTimer);
|
|
106
|
+
if (entry.retryTimer)
|
|
107
|
+
clearTimeout(entry.retryTimer);
|
|
108
|
+
(_a = entry.remove) === null || _a === void 0 ? void 0 : _a.call(entry);
|
|
109
|
+
entry.remove = undefined;
|
|
110
|
+
if (entry.book)
|
|
111
|
+
entry.buffer.push(...entry.book.finish(new Date()));
|
|
112
|
+
yield this.write(sessionId, entry);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/** Whether the session is recorded here. */
|
|
116
|
+
isRecording(sessionId) {
|
|
117
|
+
return this.running.has(sessionId);
|
|
118
|
+
}
|
|
119
|
+
open(sessionId, entry, since) {
|
|
120
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
121
|
+
var _a;
|
|
122
|
+
let clock = null;
|
|
123
|
+
try {
|
|
124
|
+
clock = yield this.readClock(entry.udid);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
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}`);
|
|
128
|
+
}
|
|
129
|
+
if (!clock) {
|
|
130
|
+
this.log.warn(`[${sessionId}] The clock of ${entry.udid} is unknown; its log lines are read as this server's time`);
|
|
131
|
+
}
|
|
132
|
+
if (entry.stopped)
|
|
133
|
+
return;
|
|
134
|
+
entry.book = new deviceLogBook_1.DeviceLogBook({ since, clock: clock !== null && clock !== void 0 ? clock : deviceLogBook_1.UNKNOWN_CLOCK });
|
|
135
|
+
yield this.listen(sessionId, entry);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
listen(sessionId, entry) {
|
|
139
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
140
|
+
var _a;
|
|
141
|
+
entry.retryTimer = undefined;
|
|
142
|
+
if (entry.stopped)
|
|
143
|
+
return;
|
|
144
|
+
let mux;
|
|
145
|
+
try {
|
|
146
|
+
mux = yield this.streams().start(entry.udid);
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
if (entry.attempts === 0) {
|
|
150
|
+
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.`);
|
|
151
|
+
}
|
|
152
|
+
this.retry(sessionId, entry);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
// Stopped while the stream started: leave it to its own idle stop.
|
|
156
|
+
if (entry.stopped)
|
|
157
|
+
return;
|
|
158
|
+
const book = entry.book;
|
|
159
|
+
let delivered = false;
|
|
160
|
+
const remove = mux.addClient((rec) => {
|
|
161
|
+
if (rec.synthetic)
|
|
162
|
+
return;
|
|
163
|
+
if (!delivered) {
|
|
164
|
+
delivered = true;
|
|
165
|
+
entry.attempts = 0;
|
|
166
|
+
}
|
|
167
|
+
const rows = book.add(rec);
|
|
168
|
+
if (rows.length === 0)
|
|
169
|
+
return;
|
|
170
|
+
entry.buffer.push(...rows);
|
|
171
|
+
if (entry.buffer.length >= exports.WRITE_BATCH)
|
|
172
|
+
void this.write(sessionId, entry);
|
|
173
|
+
},
|
|
174
|
+
// Never refused: what the session keeps is bounded by its book.
|
|
175
|
+
() => true, () => {
|
|
176
|
+
if (entry.remove === remove)
|
|
177
|
+
entry.remove = undefined;
|
|
178
|
+
if (entry.stopped)
|
|
179
|
+
return;
|
|
180
|
+
const note = book.interrupted(new Date());
|
|
181
|
+
// A stream that delivered nothing (a phone that's gone) isn't noted
|
|
182
|
+
// again on every try.
|
|
183
|
+
if (delivered) {
|
|
184
|
+
entry.buffer.push(...note);
|
|
185
|
+
this.log.info(`[${sessionId}] The device log of ${entry.udid} ended; opening it again`);
|
|
186
|
+
}
|
|
187
|
+
this.retry(sessionId, entry);
|
|
188
|
+
});
|
|
189
|
+
entry.remove = remove;
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
retry(sessionId, entry) {
|
|
193
|
+
var _a, _b;
|
|
194
|
+
if (entry.stopped || entry.retryTimer)
|
|
195
|
+
return;
|
|
196
|
+
entry.attempts += 1;
|
|
197
|
+
const delay = Math.min(this.retryFirstMs() * 2 ** (entry.attempts - 1), RETRY_MAX_MS);
|
|
198
|
+
entry.retryTimer = setTimeout(() => void this.listen(sessionId, entry), delay);
|
|
199
|
+
(_b = (_a = entry.retryTimer).unref) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Writes what is waiting, WRITE_BATCH rows at a time. On a failure the rows
|
|
203
|
+
* wait for the next write: the book bounds how many there can be.
|
|
204
|
+
*/
|
|
205
|
+
write(sessionId, entry) {
|
|
206
|
+
entry.writing = entry.writing.then(() => __awaiter(this, void 0, void 0, function* () {
|
|
207
|
+
var _a;
|
|
208
|
+
while (entry.buffer.length > 0) {
|
|
209
|
+
const batch = entry.buffer.splice(0, exports.WRITE_BATCH);
|
|
210
|
+
// The session's rows are read in `createdAt` order, so each is a
|
|
211
|
+
// millisecond after the one before: the lines of one moment (a stack
|
|
212
|
+
// trace's) keep their order whatever the database.
|
|
213
|
+
const first = Math.max(Date.now(), entry.lastCreatedAt + 1);
|
|
214
|
+
try {
|
|
215
|
+
yield this.writeLines(sessionId, batch, first);
|
|
216
|
+
entry.lastCreatedAt = first + batch.length - 1;
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
entry.buffer.unshift(...batch);
|
|
220
|
+
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}`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}));
|
|
225
|
+
return entry.writing;
|
|
226
|
+
}
|
|
227
|
+
// ---------------------------------------------------------------------
|
|
228
|
+
// Seams. Overridden by tests; the defaults are the real thing.
|
|
229
|
+
// ---------------------------------------------------------------------
|
|
230
|
+
context() {
|
|
231
|
+
return typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
232
|
+
}
|
|
233
|
+
streams() {
|
|
234
|
+
return typedi_1.Container.get(LogcatStreamService_1.LogcatStreamService);
|
|
235
|
+
}
|
|
236
|
+
flushIntervalMs() {
|
|
237
|
+
return exports.FLUSH_INTERVAL_MS;
|
|
238
|
+
}
|
|
239
|
+
retryFirstMs() {
|
|
240
|
+
return RETRY_FIRST_MS;
|
|
241
|
+
}
|
|
242
|
+
/** The phone's clock against this server's, or null if its answer isn't one. */
|
|
243
|
+
readClock(udid) {
|
|
244
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
245
|
+
const adb = yield (0, adbForPhone_1.adbForPhone)(udid);
|
|
246
|
+
const asked = Date.now();
|
|
247
|
+
// One word: the phone's shell would take a second one as a time to set.
|
|
248
|
+
const out = yield adb.adbExec(['-s', udid, 'shell', deviceLogBook_1.DEVICE_CLOCK_COMMAND], {
|
|
249
|
+
timeout: CLOCK_TIMEOUT_MS,
|
|
250
|
+
});
|
|
251
|
+
return (0, deviceLogBook_1.parseDeviceClock)(String(out !== null && out !== void 0 ? out : ''), (asked + Date.now()) / 2);
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
writeLines(sessionId, lines, firstCreatedAt) {
|
|
255
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
256
|
+
yield prisma_1.prisma.log.createMany({
|
|
257
|
+
data: lines.map((line, i) => ({
|
|
258
|
+
session_id: sessionId,
|
|
259
|
+
log_type: 'DEVICE',
|
|
260
|
+
message: line.message,
|
|
261
|
+
timestamp: line.timestamp,
|
|
262
|
+
createdAt: new Date(firstCreatedAt + i),
|
|
263
|
+
})),
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
exports.SessionDeviceLogs = SessionDeviceLogs;
|
|
269
|
+
exports.SessionDeviceLogs = SessionDeviceLogs = __decorate([
|
|
270
|
+
(0, typedi_1.Service)()
|
|
271
|
+
], SessionDeviceLogs);
|