@xenon-device-management/xenon 2.15.0 → 2.17.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 +38 -26
- package/lib/package.json +2 -1
- package/lib/public/assets/{AnnotationOverlay-Btq3dZRI.js → AnnotationOverlay-CjmMCS0S.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-C9OoAgg_.js → ApiKeyGate-B4Ob0v3C.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-DBC_xczB.js → DeviceMosaicView-DTVOB_NO.js} +1 -1
- package/lib/public/assets/{EmptyState-DqWU2-vM.js → EmptyState-DcEG-oAi.js} +1 -1
- package/lib/public/assets/{FieldGroup-CveQ3xrA.js → FieldGroup-CtnCDshE.js} +1 -1
- package/lib/public/assets/{FilterMenu-BfKDSfHz.js → FilterMenu-BK0dE9cc.js} +1 -1
- package/lib/public/assets/{Menu-CtdCybzA.js → Menu-eKsmLpKS.js} +1 -1
- package/lib/public/assets/{Modal-Bsvj1tm2.js → Modal-DPwop3Lz.js} +1 -1
- package/lib/public/assets/{RecordingPage-CRofOiyx.js → RecordingPage-CfgSkZkB.js} +1 -1
- package/lib/public/assets/{RecordingsPage-jNQHb2IR.js → RecordingsPage-Bzl9pTUY.js} +1 -1
- package/lib/public/assets/{SegmentedControl-C8HtTha-.js → SegmentedControl-B_TZTrPm.js} +1 -1
- package/lib/public/assets/{SettingCard-D8K1LZNo.js → SettingCard-DlmvZJPM.js} +1 -1
- package/lib/public/assets/{Table-zNTX92Pn.js → Table-BigPWKXi.js} +1 -1
- package/lib/public/assets/{activity-CJ8LwZq9.js → activity-CvuZdQYu.js} +1 -1
- package/lib/public/assets/{ai-settings-CgxQZqrc.js → ai-settings-lI2UJ2n7.js} +1 -1
- package/lib/public/assets/{api-keys-ECpo-zTo.js → api-keys-CPgf9XJM.js} +1 -1
- package/lib/public/assets/{apps-BjGymyf3.js → apps-DIzOvtUp.js} +1 -1
- package/lib/public/assets/{arrow-left-BWfCbeHP.js → arrow-left-DiP5ywBT.js} +1 -1
- package/lib/public/assets/{arrow-right-DC7UGf_n.js → arrow-right-DtW_8hm5.js} +1 -1
- package/lib/public/assets/{arrow-up-right-BOjwybDk.js → arrow-up-right-C89FbNU0.js} +1 -1
- package/lib/public/assets/{auth-shell-CxMTo9aH.js → auth-shell-B7sUDb--.js} +2 -2
- package/lib/public/assets/{builds-page-DOCm4HkA.js → builds-page-BBhvrMfA.js} +1 -1
- package/lib/public/assets/{button-Cg08-EMT.js → button-DugsdUw1.js} +1 -1
- package/lib/public/assets/{calendar-CA1ikLJs.js → calendar-grKd08ZH.js} +1 -1
- package/lib/public/assets/{check-BMTaH72W.js → check-ORjyNfnX.js} +1 -1
- package/lib/public/assets/{chevron-right-B2QOv9Nx.js → chevron-right-Ei2FxUXq.js} +1 -1
- package/lib/public/assets/{circle-check-CYXBoUqT.js → circle-check-Day0HjW4.js} +1 -1
- package/lib/public/assets/{circle-x-DTwnSAnl.js → circle-x-CHxSRLHp.js} +1 -1
- package/lib/public/assets/{clock-C8_HViQP.js → clock-BDHewWTx.js} +1 -1
- package/lib/public/assets/{copy-Be7LNMXV.js → copy-CTwT4I3D.js} +1 -1
- package/lib/public/assets/{device-explorer-BOqtfGXz.js → device-explorer-DlhrKJ-g.js} +1 -1
- package/lib/public/assets/{download-B56lwxWP.js → download-D3bjz_vr.js} +1 -1
- package/lib/public/assets/{forgot-password-CCU6bzLn.js → forgot-password-BNBLi023.js} +1 -1
- package/lib/public/assets/{h264Stream-C-OtmIuk.js → h264Stream-C51Au2i_.js} +1 -1
- package/lib/public/assets/index-Dk5J6HB1.js +219 -0
- package/lib/public/assets/{index-B9KQ6LsY.js → index-DncSz5LG.js} +1 -1
- package/lib/public/assets/{index-CfxsUslp.js → index-Z7_BY3pJ.js} +1 -1
- package/lib/public/assets/{input-CZHFjbSg.js → input-bTXM4l09.js} +1 -1
- package/lib/public/assets/{line-chart-Gl_Dz3X_.js → line-chart-B3YmVYyr.js} +1 -1
- package/lib/public/assets/{list-checks-CCgjeyGY.js → list-checks-BClKEj1-.js} +1 -1
- package/lib/public/assets/{lock-CkOwgDcx.js → lock-CZsenX3D.js} +1 -1
- package/lib/public/assets/{login-D5cc4w7M.js → login-B6A6tOp9.js} +1 -1
- package/lib/public/assets/{maintenance-settings-t7qr3fvX.js → maintenance-settings-CmCJGy7O.js} +1 -1
- package/lib/public/assets/{monitor-DEJixKQO.js → monitor-BdTNEglU.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CtOrk4M8.js → mouse-pointer-2-BM8CaLtt.js} +1 -1
- package/lib/public/assets/{network-Dii9qdvk.js → network-BS6vUc1x.js} +1 -1
- package/lib/public/assets/{overview-rolAACrO.js → overview-Yfk_Y7g3.js} +1 -1
- package/lib/public/assets/{page-header-COQZxJfR.js → page-header-DhXCwpml.js} +1 -1
- package/lib/public/assets/{play-DrN1Se_R.js → play-D08gP1td.js} +1 -1
- package/lib/public/assets/{plus-5aq-9uRx.js → plus-BKboorfa.js} +1 -1
- package/lib/public/assets/{profile-page-6i5kY6kF.js → profile-page-Cd9NodrA.js} +1 -1
- package/lib/public/assets/{reset-password-sp2IBrc7.js → reset-password-acLDyyDW.js} +1 -1
- package/lib/public/assets/{runbook-page-DJDjeopU.js → runbook-page-CWLkC1Fd.js} +153 -102
- package/lib/public/assets/{select-Bpyn7rWO.js → select-DZoRKNti.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BH74vmKv.js → selector-detail-redirect-DS4qrt84.js} +1 -1
- package/lib/public/assets/{selector-health-page-Cw_g218H.js → selector-health-page-TpmN61G3.js} +1 -1
- package/lib/public/assets/session-detail-page-C0wYaXqy.js +4 -0
- package/lib/public/assets/{settings-C3uyGhyc.js → settings-CxPGEY2j.js} +1 -1
- package/lib/public/assets/{stat-tile-xVqAKIlI.js → stat-tile-DUpyYgl-.js} +1 -1
- package/lib/public/assets/{tablet-BxUScAeO.js → tablet-SA02BJNQ.js} +1 -1
- package/lib/public/assets/{teams-B5ljGkY2.js → teams-jamD0Kxr.js} +1 -1
- package/lib/public/assets/{trash-2-B701i8sy.js → trash-2-DO8fv6w4.js} +1 -1
- package/lib/public/assets/{upload-C2gr44RS.js → upload-CaiGCStb.js} +1 -1
- package/lib/public/assets/{use-builds-data-NeQOFEoy.js → use-builds-data-jS-fcvVf.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DEzJq5ef.js → use-password-reset-mode-BzoenegM.js} +1 -1
- package/lib/public/assets/{users-MY6eNe6s.js → users-Bnw7Vs_B.js} +1 -1
- package/lib/public/assets/{users-D_vv_yz4.js → users-DHvDrriG.js} +1 -1
- package/lib/public/assets/{video-off-OK1lNZQn.js → video-off-FEvy0w5U.js} +1 -1
- package/lib/public/assets/{webhook-settings-BU-C9lBf.js → webhook-settings-CBVB5lxi.js} +1 -1
- package/lib/public/assets/{zap-DO9ry1iz.js → zap-BMrTJO6M.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +11 -7
- package/lib/src/InternalHttpClient.js +27 -5
- package/lib/src/XenonCapabilityManager.js +10 -3
- package/lib/src/app/index.js +2 -0
- package/lib/src/app/openapi/grid.yaml +102 -0
- package/lib/src/app/openapi/sessions.yaml +7 -5
- package/lib/src/app/routers/auth.js +2 -1
- package/lib/src/app/routers/users.js +2 -1
- package/lib/src/app/ws/nodeSocketRelay.js +77 -11
- package/lib/src/dashboard/commandLogFields.js +42 -0
- package/lib/src/dashboard/event-manager.js +23 -154
- package/lib/src/dashboard/services/failure-analysis-service.js +8 -86
- package/lib/src/dashboard/services/failureCategories.js +264 -0
- package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
- package/lib/src/device-managers/IOSDeviceManager.js +10 -0
- package/lib/src/device-utils.js +25 -8
- package/lib/src/gateway/forwardToNode.js +4 -11
- package/lib/src/gateway/hubSessionToken.js +27 -4
- package/lib/src/gateway/nodeAsk.js +57 -0
- package/lib/src/gateway/nodeSessionDeviceLogs.js +35 -0
- package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
- package/lib/src/helpers/outboundProxy.js +219 -0
- package/lib/src/index.js +21 -0
- package/lib/src/interceptors/CommandInterceptor.js +13 -15
- package/lib/src/interceptors/xenonScripts.js +28 -7
- package/lib/src/logger.js +3 -1
- package/lib/src/plugin.js +12 -4
- package/lib/src/scripts/database-check.js +3 -3
- package/lib/src/scripts/initialize-database.js +29 -33
- package/lib/src/scripts/run-migrations.js +320 -25
- package/lib/src/services/ApiKeyService.js +16 -5
- package/lib/src/services/InterceptorService.js +6 -2
- package/lib/src/services/OrphanSweeper.js +6 -0
- package/lib/src/services/SelectorStateService.js +11 -7
- package/lib/src/services/SelectorVerificationJob.js +6 -2
- package/lib/src/services/SessionLifecycleService.js +52 -23
- package/lib/src/services/SocketClient.js +11 -0
- package/lib/src/services/SocketServer.js +361 -42
- 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/healing/HealingOrchestrator.js +3 -1
- package/lib/src/services/identity/identityChanges.js +41 -0
- package/lib/src/services/interceptor/CertManager.js +50 -16
- package/lib/src/services/logcat/NodeDeviceLogStore.js +99 -0
- package/lib/src/services/logcat/NodeDeviceLogsCollector.js +164 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +279 -37
- package/lib/src/services/logcat/deviceLogBook.js +5 -4
- package/lib/src/services/logcat/iosDriverLog.js +178 -0
- package/lib/src/services/logcat/nodeDeviceLogs.js +91 -0
- package/lib/src/services/metrics/nodeMetrics.js +20 -49
- package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
- package/lib/src/services/token/JwtKeyService.js +8 -2
- package/lib/src/services/token/mcpTokenTtl.js +27 -0
- package/lib/src/sessions/LocalSession.js +29 -1
- package/lib/src/sessions/RemoteSession.js +23 -4
- package/lib/src/sessions/SessionManager.js +33 -9
- package/lib/test/helpers/driver-log.js +38 -0
- package/lib/test/helpers/fake-http-proxy.js +106 -0
- package/lib/test/helpers/fake-logcat.js +80 -0
- package/lib/test/helpers/hub-node-pair.js +227 -0
- package/lib/test/helpers/otlp-probe.js +107 -0
- package/lib/test/integration/androidDevices.spec.js +1 -1
- package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
- package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
- package/lib/test/integration/selector-state-flow.spec.js +5 -5
- package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
- package/lib/test/unit/android-adb-retry.spec.js +112 -0
- package/lib/test/unit/command-log-network-capture.spec.js +191 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +74 -11
- package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
- package/lib/test/unit/database-check.spec.js +1 -0
- package/lib/test/unit/device-utils.spec.js +13 -4
- package/lib/test/unit/event-log-session-data.spec.js +11 -4
- package/lib/test/unit/failure-analysis.spec.js +22 -8
- package/lib/test/unit/failure-categories.spec.js +24 -22
- package/lib/test/unit/failure-categorization.spec.js +393 -0
- package/lib/test/unit/healing-tier-gate.spec.js +3 -2
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +72 -1
- package/lib/test/unit/hub-node-iphone-readiness.spec.js +134 -0
- package/lib/test/unit/hub-node-session-device-logs.spec.js +194 -0
- package/lib/test/unit/hub-node-session-metrics.spec.js +24 -192
- package/lib/test/unit/hub-remote-session-heartbeat.spec.js +8 -1
- package/lib/test/unit/interceptor/CertManager.spec.js +89 -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/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/node-device-log-store.spec.js +102 -0
- package/lib/test/unit/node-device-logs-collector.spec.js +343 -0
- package/lib/test/unit/node-gateway-create-appium-server.spec.js +8 -1
- package/lib/test/unit/node-hub-session-owner.spec.js +8 -1
- package/lib/test/unit/node-session-device-logs-route.spec.js +103 -0
- package/lib/test/unit/observability-example-loki.spec.js +25 -0
- 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/port-exhaustion.spec.js +2 -1
- package/lib/test/unit/proxy-option.spec.js +358 -0
- package/lib/test/unit/run-migrations-database.spec.js +440 -0
- package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
- package/lib/test/unit/run-migrations.test.js +264 -0
- package/lib/test/unit/selector-health-actions.spec.js +3 -1
- package/lib/test/unit/selector-state-service.spec.js +72 -23
- package/lib/test/unit/selector-verification-clean-build.spec.js +1 -1
- package/lib/test/unit/selector-verification-job.spec.js +15 -6
- 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/session-device-logs-hub-node.spec.js +340 -0
- package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
- package/lib/test/unit/session-device-logs-lifecycle.spec.js +137 -0
- package/lib/test/unit/session-device-logs.spec.js +43 -96
- package/lib/test/unit/session-end-failure-analysis.spec.js +5 -5
- package/lib/test/unit/session-log-indexes.spec.js +16 -1
- 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/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 +2 -2
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
- package/schema.json +11 -7
- 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/index-BOxe43p1.js +0 -219
- package/lib/public/assets/session-detail-page-AZJE6XXE.js +0 -4
- package/lib/src/scripts/prepare-prisma.js +0 -89
|
@@ -45,80 +45,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
45
45
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
46
|
};
|
|
47
47
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
-
exports.MAX_CONCURRENT_FAILURE_ANALYSES =
|
|
48
|
+
exports.MAX_CONCURRENT_FAILURE_ANALYSES = void 0;
|
|
49
49
|
exports.categorizeSessionFailure = categorizeSessionFailure;
|
|
50
50
|
exports.explainSessionFailure = explainSessionFailure;
|
|
51
51
|
const prisma_1 = require("../../prisma");
|
|
52
52
|
const logger_1 = __importDefault(require("../../logger"));
|
|
53
|
-
|
|
54
|
-
* Each category and the texts that file a failure under it, tried in this
|
|
55
|
-
* order against the failure reason and the last five failed commands. The
|
|
56
|
-
* dashboard's runbooks describe a category by these, not by its name
|
|
57
|
-
* (failure-categories.spec.ts).
|
|
58
|
-
*/
|
|
59
|
-
exports.ERROR_PATTERNS = [
|
|
60
|
-
{
|
|
61
|
-
category: 'ELEMENT_NOT_FOUND',
|
|
62
|
-
patterns: [
|
|
63
|
-
'NoSuchElementError',
|
|
64
|
-
'unable to find an element',
|
|
65
|
-
'An element could not be located',
|
|
66
|
-
'no such element',
|
|
67
|
-
],
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
category: 'APP_CRASH',
|
|
71
|
-
patterns: [
|
|
72
|
-
'Appium crashed',
|
|
73
|
-
'process has died',
|
|
74
|
-
'activity has died',
|
|
75
|
-
'The application has crashed',
|
|
76
|
-
'Application not responding',
|
|
77
|
-
"org.openqa.selenium.WebDriverException: An unknown server-side error occurred while processing the command. Original error: The application under test with bundle id '.*' is not running or cannot be found",
|
|
78
|
-
],
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
category: 'TIMEOUT',
|
|
82
|
-
patterns: ['timeout', 'timed out', 'TimeoutException', 'New Command Timeout', 'socket hang up'],
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
category: 'PERMISSION_BLOCKED',
|
|
86
|
-
patterns: ['Permission alert', 'Security alert', 'Always Allow', 'Allow while using app'],
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
category: 'WDA_FAILURE',
|
|
90
|
-
patterns: [
|
|
91
|
-
'WebDriverAgent',
|
|
92
|
-
'WDA',
|
|
93
|
-
'xcodebuild failed',
|
|
94
|
-
'crashed with code',
|
|
95
|
-
'Unable to connect to WDA',
|
|
96
|
-
'Session does not exist',
|
|
97
|
-
'the session is not in a running state',
|
|
98
|
-
],
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
category: 'XENON_COMMAND_FAILURE',
|
|
102
|
-
patterns: ['Command failed', 'telemetry failed', 'interceptor error'],
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
category: 'SYSTEM_OVERLOAD',
|
|
106
|
-
patterns: ['OutOfMemory', 'MemoryLimit', 'thermal throttling', 'too many open files'],
|
|
107
|
-
},
|
|
108
|
-
];
|
|
109
|
-
/** What the analysis writes when no pattern matches. */
|
|
110
|
-
const UNMATCHED = 'UNKNOWN';
|
|
111
|
-
/**
|
|
112
|
-
* Every category the analysis writes to a failed session's
|
|
113
|
-
* `failure_category`, upper case as stored. The only other value the column
|
|
114
|
-
* holds is `HUB_RESTART_CATEGORY` (SessionManager). The API reference's
|
|
115
|
-
* examples and the dashboard's runbooks are held to these
|
|
116
|
-
* (failure-categories.spec.ts).
|
|
117
|
-
*/
|
|
118
|
-
exports.ANALYSIS_CATEGORIES = [
|
|
119
|
-
...exports.ERROR_PATTERNS.map((p) => p.category),
|
|
120
|
-
UNMATCHED,
|
|
121
|
-
];
|
|
53
|
+
const failureCategories_1 = require("./failureCategories");
|
|
122
54
|
/**
|
|
123
55
|
* How many failed sessions' AI analyses run at once (explainSessionFailure);
|
|
124
56
|
* the others wait their turn. They run after their sessions have ended, so
|
|
@@ -150,8 +82,9 @@ function endAnalysisTurn() {
|
|
|
150
82
|
}
|
|
151
83
|
/**
|
|
152
84
|
* Files a failed session under a category (`failure_category`) by its failure
|
|
153
|
-
* reason and its failed commands
|
|
154
|
-
*
|
|
85
|
+
* reason and its last five failed commands' errors (categorizeFailure, in
|
|
86
|
+
* failureCategories.ts). Rules only, so it is quick: a session's end waits for
|
|
87
|
+
* it. Never throws.
|
|
155
88
|
*/
|
|
156
89
|
function categorizeSessionFailure(sessionId) {
|
|
157
90
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -164,22 +97,11 @@ function categorizeSessionFailure(sessionId) {
|
|
|
164
97
|
});
|
|
165
98
|
if (!session)
|
|
166
99
|
return;
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
const combined_text = (reason + ' ' + logs_text).toLowerCase();
|
|
170
|
-
let identifiedCategory = UNMATCHED;
|
|
171
|
-
for (const item of exports.ERROR_PATTERNS) {
|
|
172
|
-
if (item.patterns.some((p) => new RegExp(p, 'i').test(combined_text))) {
|
|
173
|
-
identifiedCategory = item.category;
|
|
174
|
-
break;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
// Special case for App Crash - check Logcat/Syslog if available
|
|
178
|
-
// For now we rely on the error response text which usually mentions "process has died"
|
|
179
|
-
logger_1.default.info(`[FailureAnalysis] Session ${sessionId} identified as ${identifiedCategory}`);
|
|
100
|
+
const category = (0, failureCategories_1.categorizeFailure)(session.failure_reason || '', session.SessionLog.map((l) => (0, failureCategories_1.commandErrorOf)(l.response)));
|
|
101
|
+
logger_1.default.info(`[FailureAnalysis] Session ${sessionId} identified as ${category}`);
|
|
180
102
|
yield prisma_1.prisma.session.update({
|
|
181
103
|
where: { id: sessionId },
|
|
182
|
-
data: { failure_category:
|
|
104
|
+
data: { failure_category: category },
|
|
183
105
|
});
|
|
184
106
|
}
|
|
185
107
|
catch (err) {
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* What files a failed session under a `failure_category`, and how.
|
|
4
|
+
*
|
|
5
|
+
* The texts here are those the installed drivers, Appium and Xenon send
|
|
6
|
+
* (UiAutomator2 6.4.1 with its server 9.3.1, XCUITest 10.9.0 with
|
|
7
|
+
* WebDriverAgent 10.2.3, base-driver 10.x), plus a few a test may report in
|
|
8
|
+
* its own words (`xenon: setSessionStatus`), marked as such.
|
|
9
|
+
*
|
|
10
|
+
* Through 2.15 the rules were guesses:
|
|
11
|
+
* - none of App crash's texts existed anywhere;
|
|
12
|
+
* - a raw body's stack trace was matched too. Every UiAutomator2 error a hub
|
|
13
|
+
* records carries netty's `IdleStateHandler`, so one that matched no
|
|
14
|
+
* earlier rule was filed TIMEOUT;
|
|
15
|
+
* - the five failed commands were read as one text, so a failure the test
|
|
16
|
+
* had recovered from could decide the category.
|
|
17
|
+
*
|
|
18
|
+
* A rule matches a W3C error code exactly (`codes`), or a phrase anywhere in
|
|
19
|
+
* the reason or a command's error or message (`phrases`), ignoring case.
|
|
20
|
+
* Codes are only on the rows a hub records for a node's sessions: this
|
|
21
|
+
* server's own rows hold the message alone, so every category needs phrases
|
|
22
|
+
* too. Xenon's own messages are read first, by how they start, since they
|
|
23
|
+
* quote the test's selector (xenonOwnCategory). Rules are tried in this order.
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.RETIRED_CATEGORIES = exports.ANALYSIS_CATEGORIES = exports.FAILURE_RULES = exports.UNMATCHED_CATEGORY = exports.HUB_RESTART_CATEGORY = void 0;
|
|
27
|
+
exports.xenonOwnCategory = xenonOwnCategory;
|
|
28
|
+
exports.commandErrorOf = commandErrorOf;
|
|
29
|
+
exports.categorizeFailure = categorizeFailure;
|
|
30
|
+
/** A session the server's restart or shutdown ended. SessionManager also writes it at boot. */
|
|
31
|
+
exports.HUB_RESTART_CATEGORY = 'HUB_RESTART';
|
|
32
|
+
/** What a failure that matches no rule is filed as. */
|
|
33
|
+
exports.UNMATCHED_CATEGORY = 'UNKNOWN';
|
|
34
|
+
exports.FAILURE_RULES = [
|
|
35
|
+
{
|
|
36
|
+
// ShutdownCoordinator's drain: the whole reason, never a phrase in one.
|
|
37
|
+
category: exports.HUB_RESTART_CATEGORY,
|
|
38
|
+
codes: ['Hub shutdown'],
|
|
39
|
+
phrases: [],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
// Before SESSION_LOST: a proxied connect that fails with EMFILE ("Could
|
|
43
|
+
// not proxy command ... connect EMFILE") is the server out of files, not
|
|
44
|
+
// the helper gone. Not a bare "EMFILE": that is a substring of a locator
|
|
45
|
+
// such as "systemFileList".
|
|
46
|
+
category: 'SYSTEM_OVERLOAD',
|
|
47
|
+
codes: [],
|
|
48
|
+
phrases: ['OutOfMemory', 'too many open files', 'connect EMFILE'],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
// The phone's automation helper (WebDriverAgent, the UiAutomator2
|
|
52
|
+
// server, Chromedriver) or the session itself went away during the run.
|
|
53
|
+
category: 'SESSION_LOST',
|
|
54
|
+
codes: ['invalid session id'],
|
|
55
|
+
phrases: [
|
|
56
|
+
// base-driver's proxy, when the helper doesn't answer at all (socket
|
|
57
|
+
// hang up, ECONNREFUSED, a read time-out).
|
|
58
|
+
'Could not proxy command to the remote server',
|
|
59
|
+
// UiAutomator2: its server's instrumentation exited ("probably crashed"
|
|
60
|
+
// is the helper, not the app), or the phone went away.
|
|
61
|
+
'instrumentation process is not running',
|
|
62
|
+
// WebDriverAgent restarted and lost the session.
|
|
63
|
+
'Session does not exist',
|
|
64
|
+
'A session is either terminated or not started',
|
|
65
|
+
// Appium's own, when a command races the session's end.
|
|
66
|
+
'The session with id',
|
|
67
|
+
// Appium ended the session itself (onUnexpectedShutdown).
|
|
68
|
+
'unexpectedly shut down',
|
|
69
|
+
'Driver shut down unexpectedly',
|
|
70
|
+
'Chromedriver quit unexpectedly',
|
|
71
|
+
// XCUITest's commandTimeouts, which then ends the session.
|
|
72
|
+
'Appium did not get any response from',
|
|
73
|
+
// Xenon lost the session: OrphanSweeper, SessionHeartbeatService, and
|
|
74
|
+
// the hub's answer for a node it can't reach (forwardToNode).
|
|
75
|
+
'Session heartbeat timeout',
|
|
76
|
+
'Session terminal failure',
|
|
77
|
+
'The node no longer has this session',
|
|
78
|
+
'Node session status failed',
|
|
79
|
+
'could not reach the node running this session',
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
category: 'APP_CRASH',
|
|
84
|
+
codes: [],
|
|
85
|
+
phrases: [
|
|
86
|
+
// WebDriverAgent, when the app under test is gone.
|
|
87
|
+
'is not running, possibly crashed',
|
|
88
|
+
// A test's own words.
|
|
89
|
+
'The application has crashed',
|
|
90
|
+
'Application not responding',
|
|
91
|
+
'process has died',
|
|
92
|
+
'activity has died',
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
category: 'STALE_ELEMENT',
|
|
97
|
+
codes: ['stale element reference'],
|
|
98
|
+
phrases: [
|
|
99
|
+
// base-driver's and WebDriverAgent's default, and the UiAutomator2
|
|
100
|
+
// server's.
|
|
101
|
+
'no longer attached to the DOM',
|
|
102
|
+
// The UiAutomator2 server's cache. "not present in the cache or has
|
|
103
|
+
// expired" is answered as "no such element", hence this rule first.
|
|
104
|
+
'does not exist in DOM anymore',
|
|
105
|
+
'do not exist in DOM anymore',
|
|
106
|
+
'is not linked to the same object in DOM anymore',
|
|
107
|
+
'is not present in the cache or has expired',
|
|
108
|
+
'StaleObjectException',
|
|
109
|
+
// WebDriverAgent's cache and snapshots.
|
|
110
|
+
'expired from the internal cache',
|
|
111
|
+
'is not present in the current view anymore',
|
|
112
|
+
// Safari and web views on an iPhone.
|
|
113
|
+
'Element does not exist in cache',
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
// Specific phrases only: a bare "timeout" also matched Xenon's own
|
|
118
|
+
// autowait misses, a hung helper, heartbeat reasons and command titles.
|
|
119
|
+
category: 'TIMEOUT',
|
|
120
|
+
codes: ['timeout', 'script timeout'],
|
|
121
|
+
phrases: [
|
|
122
|
+
'New Command Timeout of',
|
|
123
|
+
'timed out due to inactivity',
|
|
124
|
+
// base-driver's defaults for `timeout` and `script timeout`.
|
|
125
|
+
'did not complete before its timeout expired',
|
|
126
|
+
'Timed out waiting for asynchronous script result',
|
|
127
|
+
// XCUITest and WebDriverAgent.
|
|
128
|
+
'waiting for XCTest to complete',
|
|
129
|
+
'cannot be launched within',
|
|
130
|
+
'Timed out while waiting until the screen gets locked',
|
|
131
|
+
'Did not receive any expected',
|
|
132
|
+
// UiAutomator2, when the app never lets the screen go idle (an endless
|
|
133
|
+
// animation, a video, a busy main thread) and it can't be read.
|
|
134
|
+
'hogging the main UI thread',
|
|
135
|
+
// A test's own words (Selenium's exception).
|
|
136
|
+
'TimeoutException',
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
category: 'ELEMENT_NOT_FOUND',
|
|
141
|
+
codes: ['no such element'],
|
|
142
|
+
phrases: [
|
|
143
|
+
'An element could not be located',
|
|
144
|
+
// WebDriverAgent's identifier and predicate lookups.
|
|
145
|
+
"didn't match any elements",
|
|
146
|
+
// A test's own words (NoSuchElementException).
|
|
147
|
+
'NoSuchElement',
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
// No native driver reports a blocking prompt: it shows up as Element not
|
|
152
|
+
// found. Only web pages on an iPhone, and a test's own words.
|
|
153
|
+
category: 'PERMISSION_BLOCKED',
|
|
154
|
+
codes: ['unexpected alert open'],
|
|
155
|
+
phrases: [
|
|
156
|
+
'A modal dialog was open',
|
|
157
|
+
'Permission alert',
|
|
158
|
+
'Security alert',
|
|
159
|
+
'Always Allow',
|
|
160
|
+
'Allow while using app',
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
/** Every category the failure analysis writes, upper case as stored. */
|
|
165
|
+
exports.ANALYSIS_CATEGORIES = [
|
|
166
|
+
...exports.FAILURE_RULES.map((r) => r.category),
|
|
167
|
+
exports.UNMATCHED_CATEGORY,
|
|
168
|
+
];
|
|
169
|
+
/**
|
|
170
|
+
* Categories no longer written, which sessions filed through 2.15 still hold.
|
|
171
|
+
* Their runbooks stay. WDA_FAILURE's live cases are SESSION_LOST now, and
|
|
172
|
+
* XENON_COMMAND_FAILURE matched nothing real but base-driver's default
|
|
173
|
+
* stale-element text, which is STALE_ELEMENT.
|
|
174
|
+
*/
|
|
175
|
+
exports.RETIRED_CATEGORIES = ['WDA_FAILURE', 'XENON_COMMAND_FAILURE'];
|
|
176
|
+
/**
|
|
177
|
+
* The category of a message Xenon itself throws, by how it starts. These
|
|
178
|
+
* quote the test's selector, which may hold any rule's phrase ("Allow
|
|
179
|
+
* notifications to be enabled"), so they are read before the rules.
|
|
180
|
+
*/
|
|
181
|
+
function xenonOwnCategory(text) {
|
|
182
|
+
const t = text.trim();
|
|
183
|
+
// CommandInterceptor's autowait: an element that never became enabled, or
|
|
184
|
+
// one that was never found.
|
|
185
|
+
if (t.startsWith('Autowait timed out after ')) {
|
|
186
|
+
return t.endsWith(' to be enabled') ? 'TIMEOUT' : 'ELEMENT_NOT_FOUND';
|
|
187
|
+
}
|
|
188
|
+
// OmniVision's miss, and an element id Xenon doesn't hold.
|
|
189
|
+
if (t.startsWith('Xenon found nothing on the screen matching '))
|
|
190
|
+
return 'ELEMENT_NOT_FOUND';
|
|
191
|
+
if (t.startsWith('Xenon has no element '))
|
|
192
|
+
return 'ELEMENT_NOT_FOUND';
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* The error and message of a failed command's recorded answer, and nothing
|
|
197
|
+
* else: never its stack trace, which names netty's IdleStateHandler on every
|
|
198
|
+
* UiAutomator2 error and the driver on every error.
|
|
199
|
+
*/
|
|
200
|
+
function commandErrorOf(response) {
|
|
201
|
+
var _a;
|
|
202
|
+
if (!response)
|
|
203
|
+
return {};
|
|
204
|
+
try {
|
|
205
|
+
const value = (_a = JSON.parse(response)) === null || _a === void 0 ? void 0 : _a.value;
|
|
206
|
+
if (!value || typeof value !== 'object')
|
|
207
|
+
return {};
|
|
208
|
+
return {
|
|
209
|
+
error: typeof value.error === 'string' ? value.error : undefined,
|
|
210
|
+
message: typeof value.message === 'string' ? value.message : undefined,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
catch (_b) {
|
|
214
|
+
return {};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const norm = (s) => s.trim().toLowerCase();
|
|
218
|
+
function ruleFor(texts, codes) {
|
|
219
|
+
for (const t of texts) {
|
|
220
|
+
const own = xenonOwnCategory(t);
|
|
221
|
+
if (own)
|
|
222
|
+
return own;
|
|
223
|
+
}
|
|
224
|
+
const haystack = texts.map(norm);
|
|
225
|
+
const codeSet = new Set(codes.map(norm));
|
|
226
|
+
for (const rule of exports.FAILURE_RULES) {
|
|
227
|
+
if (rule.codes.some((c) => codeSet.has(norm(c))))
|
|
228
|
+
return rule.category;
|
|
229
|
+
if (rule.phrases.some((p) => haystack.some((t) => t.includes(norm(p)))))
|
|
230
|
+
return rule.category;
|
|
231
|
+
}
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* A failed session's category, from its failure reason and its failed
|
|
236
|
+
* commands (newest first).
|
|
237
|
+
*
|
|
238
|
+
* The failure that ended the session decides alone: the reason, read with the
|
|
239
|
+
* newest failed command when the reason was taken from it (for a session on
|
|
240
|
+
* a node's phone the reason is that command's bare code, "no such element",
|
|
241
|
+
* and its message says more). A reason no rule matches is UNKNOWN: an earlier
|
|
242
|
+
* failure the test recovered from, or a find it expected to fail, doesn't
|
|
243
|
+
* decide. Only a session with no reason at all is read from its failed
|
|
244
|
+
* commands, newest first.
|
|
245
|
+
*/
|
|
246
|
+
function categorizeFailure(reason, failedCommands) {
|
|
247
|
+
const said = (c) => [c.error, c.message].filter((s) => !!s);
|
|
248
|
+
const codesOf = (c) => (c.error ? [c.error] : []);
|
|
249
|
+
const r = reason.trim();
|
|
250
|
+
if (r) {
|
|
251
|
+
const newest = failedCommands[0];
|
|
252
|
+
const fromNewest = !!newest && said(newest).some((s) => norm(s) === norm(r));
|
|
253
|
+
const category = fromNewest
|
|
254
|
+
? ruleFor([r, ...said(newest)], codesOf(newest))
|
|
255
|
+
: ruleFor([r], [r]);
|
|
256
|
+
return category !== null && category !== void 0 ? category : exports.UNMATCHED_CATEGORY;
|
|
257
|
+
}
|
|
258
|
+
for (const c of failedCommands) {
|
|
259
|
+
const category = ruleFor(said(c), codesOf(c));
|
|
260
|
+
if (category)
|
|
261
|
+
return category;
|
|
262
|
+
}
|
|
263
|
+
return exports.UNMATCHED_CATEGORY;
|
|
264
|
+
}
|
|
@@ -90,6 +90,11 @@ const typedi_2 = require("typedi");
|
|
|
90
90
|
* they had worked. Within the 15 minutes a hub gives a node's install.
|
|
91
91
|
*/
|
|
92
92
|
exports.ANDROID_INSTALL_TIMEOUT_MS = 10 * 60000;
|
|
93
|
+
/** How long after adb, or its device tracking, failed to start it is tried again. */
|
|
94
|
+
const ADB_RETRY_MS = 60000;
|
|
95
|
+
function reasonOf(e) {
|
|
96
|
+
return e instanceof Error ? e.message : String(e);
|
|
97
|
+
}
|
|
93
98
|
/**
|
|
94
99
|
* Whether a server set to `androidDeviceType` serves this phone: real phones,
|
|
95
100
|
* emulators ('simulated'), or both, and with `bootedEmulators` only the
|
|
@@ -110,6 +115,11 @@ let AndroidDeviceManager = class AndroidDeviceManager {
|
|
|
110
115
|
this.context = context;
|
|
111
116
|
this.log = logger_1.default.scope('AndroidManager');
|
|
112
117
|
this.adbAvailable = true;
|
|
118
|
+
// When adb last failed to start, discovery answers no phones until this
|
|
119
|
+
// time, then tries again.
|
|
120
|
+
this.adbRetryAt = 0;
|
|
121
|
+
// When adb's device tracking last failed to start, when to try it again.
|
|
122
|
+
this.trackerRetryAt = 0;
|
|
113
123
|
this.abortControl = new Map();
|
|
114
124
|
this.tracker = undefined;
|
|
115
125
|
this.remoteTrackers = [];
|
|
@@ -161,8 +171,11 @@ let AndroidDeviceManager = class AndroidDeviceManager {
|
|
|
161
171
|
return __awaiter(this, void 0, void 0, function* () {
|
|
162
172
|
var _a;
|
|
163
173
|
if (!this.adbAvailable) {
|
|
164
|
-
|
|
165
|
-
|
|
174
|
+
if (Date.now() < this.adbRetryAt) {
|
|
175
|
+
logger_1.default.debug('adb is not available. So, returning empty list');
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
this.adbAvailable = true;
|
|
166
179
|
}
|
|
167
180
|
let devices = [];
|
|
168
181
|
try {
|
|
@@ -401,32 +414,57 @@ let AndroidDeviceManager = class AndroidDeviceManager {
|
|
|
401
414
|
}
|
|
402
415
|
getAdb() {
|
|
403
416
|
return __awaiter(this, void 0, void 0, function* () {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
417
|
+
// One start at a time: calls that come while adb or its tracker is
|
|
418
|
+
// starting wait for that start. Two discoveries at once each started a
|
|
419
|
+
// tracker, and each tracker handled every plug event again.
|
|
420
|
+
if (!this.adbStarting) {
|
|
421
|
+
this.adbStarting = this.startAdb().finally(() => {
|
|
422
|
+
this.adbStarting = undefined;
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
yield this.adbStarting;
|
|
426
|
+
return { adbInstance: this.adb, adbTracker: this.tracker };
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
startAdb() {
|
|
430
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
431
|
+
if (!this.adb) {
|
|
432
|
+
try {
|
|
433
|
+
this.adb = yield appium_adb_1.ADB.createADB({});
|
|
434
|
+
}
|
|
435
|
+
catch (e) {
|
|
436
|
+
this.adbUnavailable('Could not start adb', e);
|
|
421
437
|
}
|
|
422
438
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
439
|
+
// Plug events. Without them discovery still lists phones each time it
|
|
440
|
+
// runs, so a failure here hides none; it is tried again a minute later.
|
|
441
|
+
if (this.adb &&
|
|
442
|
+
!this.tracker &&
|
|
443
|
+
process.env.NODE_ENV !== 'test' &&
|
|
444
|
+
Date.now() >= this.trackerRetryAt) {
|
|
445
|
+
try {
|
|
446
|
+
const tracker = yield adbkit_1.default.createClient().trackDevices();
|
|
447
|
+
yield this.createLocalAdbTracker(tracker, this.adb)();
|
|
448
|
+
this.tracker = tracker;
|
|
449
|
+
}
|
|
450
|
+
catch (e) {
|
|
451
|
+
this.trackerRetryAt = Date.now() + ADB_RETRY_MS;
|
|
452
|
+
this.log.error(`Could not follow Android plug events through adb: ${reasonOf(e)}. ` +
|
|
453
|
+
`Trying again in ${ADB_RETRY_MS / 1000} s.`);
|
|
454
|
+
}
|
|
426
455
|
}
|
|
427
|
-
return { adbInstance: this.adb, adbTracker: this.tracker };
|
|
428
456
|
});
|
|
429
457
|
}
|
|
458
|
+
/**
|
|
459
|
+
* Discovery answers no phones for a minute, then tries again: adb may work
|
|
460
|
+
* by then (an SDK installed, a stuck adb server restarted). Through 2.15 it
|
|
461
|
+
* never tried again before a restart, and didn't say why.
|
|
462
|
+
*/
|
|
463
|
+
adbUnavailable(what, e) {
|
|
464
|
+
this.adbAvailable = false;
|
|
465
|
+
this.adbRetryAt = Date.now() + ADB_RETRY_MS;
|
|
466
|
+
this.log.error(`${what}: ${reasonOf(e)}. Trying again in ${ADB_RETRY_MS / 1000} s.`);
|
|
467
|
+
}
|
|
430
468
|
getAdbForDevice(udid) {
|
|
431
469
|
return __awaiter(this, void 0, void 0, function* () {
|
|
432
470
|
const { adbInstance } = yield this.getAdb();
|
|
@@ -266,6 +266,16 @@ let IOSDeviceManager = class IOSDeviceManager {
|
|
|
266
266
|
var _a;
|
|
267
267
|
if (device.cloud)
|
|
268
268
|
return true;
|
|
269
|
+
// A simulator's WebDriverAgent is the XCUITest driver's: it builds,
|
|
270
|
+
// installs and launches it on the session's wdaLocalPort (iOSCapabilities)
|
|
271
|
+
// once Appium creates the session, after this check, and boots a shut-down
|
|
272
|
+
// simulator itself. Xenon runs WDA only on an iPhone (go-ios runwda), so
|
|
273
|
+
// asking for it here refused every simulator whose WDA wasn't already up,
|
|
274
|
+
// and recoverHealth's reboot never starts one. Through 2.15 it did.
|
|
275
|
+
if (!device.realDevice) {
|
|
276
|
+
this.log.info(`[SessionStart] ${device.udid} is a simulator: the driver starts its WebDriverAgent`);
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
269
279
|
this.log.info(`🚀 [SessionStart] Verifying readiness for ${device.udid}...`);
|
|
270
280
|
const streamService = typedi_1.Container.get(IOSStreamService_1.default);
|
|
271
281
|
const streamStatus = streamService.getStreamStatus(device.udid);
|
package/lib/src/device-utils.js
CHANGED
|
@@ -87,6 +87,7 @@ const device_managers_1 = require("./device-managers");
|
|
|
87
87
|
const device_service_1 = require("./data-service/device-service");
|
|
88
88
|
const deviceClaims_1 = require("./data-service/deviceClaims");
|
|
89
89
|
const PluginContext_1 = require("./PluginContext");
|
|
90
|
+
const TracingService_1 = require("./services/TracingService");
|
|
90
91
|
const logger_1 = __importDefault(require("./logger"));
|
|
91
92
|
const Platform_1 = __importDefault(require("./enums/Platform"));
|
|
92
93
|
const lodash_1 = __importDefault(require("lodash"));
|
|
@@ -250,7 +251,7 @@ function allocateDeviceForSession(capability_1, deviceTimeOutMs_1, deviceQueryIn
|
|
|
250
251
|
if (device !== null)
|
|
251
252
|
return true;
|
|
252
253
|
}
|
|
253
|
-
logger_1.default.info(`Waiting for free device. Filter: ${JSON.stringify(filters)}
|
|
254
|
+
logger_1.default.info(`Waiting for free device. Filter: ${JSON.stringify(filters)}`);
|
|
254
255
|
return false;
|
|
255
256
|
}));
|
|
256
257
|
}), { timeout, intervalBetweenAttempts });
|
|
@@ -269,13 +270,22 @@ function allocateDeviceForSession(capability_1, deviceTimeOutMs_1, deviceQueryIn
|
|
|
269
270
|
else if (possibleDevice && (0, device_service_1.isDeviceReserved)(possibleDevice)) {
|
|
270
271
|
failureReason = `Device is reserved by ${possibleDevice.reservedBy}.`;
|
|
271
272
|
}
|
|
272
|
-
throw new Error(`${failureReason}
|
|
273
|
+
throw new Error(`${failureReason} Device request: ${JSON.stringify(filterCopy)}`);
|
|
273
274
|
}
|
|
274
275
|
// We have a locked device here
|
|
275
276
|
if (device !== null) {
|
|
276
277
|
const lockedDevice = device;
|
|
277
|
-
//
|
|
278
|
-
|
|
278
|
+
// Ready for a session: asked only of a phone this server drives. The
|
|
279
|
+
// check is this machine's (an iPhone's WebDriverAgent on 127.0.0.1, and a
|
|
280
|
+
// go-ios stream started to recover it). Another server's phone, a node's
|
|
281
|
+
// on a hub, is checked by that server when it allocates the phone the hub
|
|
282
|
+
// pins. Through 2.15 the hub checked it too: with the node on another
|
|
283
|
+
// machine nothing answered and the session was refused as unhealthy, and
|
|
284
|
+
// with both on one Mac the hub started a WebDriverAgent of its own on the
|
|
285
|
+
// node's iPhone.
|
|
286
|
+
const context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
287
|
+
const ownPhone = (0, localDeviceHosts_1.isOwnDevice)((0, localDeviceHosts_1.localDeviceHosts)(context.pluginArgs, context.port), context.nodeId, lockedDevice);
|
|
288
|
+
if (!lockedDevice.cloud && ownPhone) {
|
|
279
289
|
const platform = lockedDevice.platform.toLowerCase();
|
|
280
290
|
const managers = yield getDeviceManager().deviceInstances();
|
|
281
291
|
const manager = managers.find((m) => {
|
|
@@ -648,7 +658,7 @@ function unblockCandidateDevices() {
|
|
|
648
658
|
}
|
|
649
659
|
function releaseBlockedDevices(newCommandTimeout) {
|
|
650
660
|
return __awaiter(this, void 0, void 0, function* () {
|
|
651
|
-
var _a, _b
|
|
661
|
+
var _a, _b;
|
|
652
662
|
const busyDevices = yield unblockCandidateDevices();
|
|
653
663
|
// A phone an SDK lease holds is the lease's to time out (its heartbeats and
|
|
654
664
|
// expiry, LeaseOrphanSweeper), not idle until a session runs on it. A lease
|
|
@@ -696,9 +706,16 @@ function releaseBlockedDevices(newCommandTimeout) {
|
|
|
696
706
|
if (timeSinceLastCmdExecuted > timeoutSeconds) {
|
|
697
707
|
// unblock regardless of whether the device has session or not
|
|
698
708
|
logger_1.default.info(`Unblocking device ${device.udid} at host ${device.host} because it has been idle for ${timeSinceLastCmdExecuted} seconds`);
|
|
709
|
+
const idleSession = (_a = device.claimSessionId) !== null && _a !== void 0 ? _a : device.session_id;
|
|
699
710
|
// Before anything below can release the phone (onSessionStopped too).
|
|
700
|
-
yield restoreIdleSessionNetwork(
|
|
701
|
-
yield forgetIdleSessionMemory(
|
|
711
|
+
yield restoreIdleSessionNetwork(idleSession);
|
|
712
|
+
yield forgetIdleSessionMemory(idleSession);
|
|
713
|
+
if (idleSession) {
|
|
714
|
+
typedi_1.Container.get(TracingService_1.TracingService).endSessionSpan(idleSession, {
|
|
715
|
+
failed: true,
|
|
716
|
+
reason: 'Session timed out due to inactivity',
|
|
717
|
+
});
|
|
718
|
+
}
|
|
702
719
|
// Principal Protection: If this device has an active dashboard session, stop it properly
|
|
703
720
|
if (device.session_id) {
|
|
704
721
|
const sessionId = device.session_id;
|
|
@@ -714,7 +731,7 @@ function releaseBlockedDevices(newCommandTimeout) {
|
|
|
714
731
|
}
|
|
715
732
|
// Keyed on the session read here, so a release that loses the race to
|
|
716
733
|
// the session's own end (and the phone's next claim) frees nothing.
|
|
717
|
-
const claimant = (
|
|
734
|
+
const claimant = (_b = device.claimSessionId) !== null && _b !== void 0 ? _b : device.session_id;
|
|
718
735
|
yield (claimant
|
|
719
736
|
? (0, device_service_1.releaseSessionDevice)(device.udid, device.host, claimant)
|
|
720
737
|
: (0, device_service_1.unblockDevice)(device.udid, device.host)).catch((err) => logger_1.default.error(`Unable to release ${device.udid}: ${err}`));
|
|
@@ -11,11 +11,10 @@ exports.readAnswer = readAnswer;
|
|
|
11
11
|
exports.sendAnswer = sendAnswer;
|
|
12
12
|
const http_1 = __importDefault(require("http"));
|
|
13
13
|
const https_1 = __importDefault(require("https"));
|
|
14
|
-
const https_proxy_agent_1 = require("https-proxy-agent");
|
|
15
|
-
const http_proxy_agent_1 = require("http-proxy-agent");
|
|
16
14
|
const internalCall_1 = require("./internalCall");
|
|
17
15
|
const hubSessionToken_1 = require("./hubSessionToken");
|
|
18
16
|
const healReport_1 = require("./healReport");
|
|
17
|
+
const outboundProxy_1 = require("../helpers/outboundProxy");
|
|
19
18
|
/**
|
|
20
19
|
* Sending a session command on to the node that runs the session, and handing
|
|
21
20
|
* the node's answer back to the client unchanged.
|
|
@@ -60,17 +59,11 @@ function forwardedRequestHeaders(incoming) {
|
|
|
60
59
|
out['accept-encoding'] = 'identity';
|
|
61
60
|
return out;
|
|
62
61
|
}
|
|
63
|
-
/** HTTP(S)_PROXY from the environment, as the old command proxy honoured it. */
|
|
64
|
-
function proxyAgentFor(url) {
|
|
65
|
-
const proxy = process.env.HTTP_PROXY || process.env.HTTPS_PROXY;
|
|
66
|
-
if (!proxy)
|
|
67
|
-
return undefined;
|
|
68
|
-
return url.protocol === 'https:' ? new https_proxy_agent_1.HttpsProxyAgent(proxy) : new http_proxy_agent_1.HttpProxyAgent(proxy);
|
|
69
|
-
}
|
|
70
62
|
/**
|
|
71
63
|
* Send one request to a node and resolve with its response stream. No retry:
|
|
72
64
|
* a WebDriver command is not safe to repeat. Credentials in the URL (a cloud
|
|
73
|
-
* provider's) become basic auth.
|
|
65
|
+
* provider's) become basic auth. Through the environment's proxy for the URL,
|
|
66
|
+
* by axios's rule (helpers/outboundProxy.ts).
|
|
74
67
|
*/
|
|
75
68
|
function sendToNode(request) {
|
|
76
69
|
const url = new URL(request.url);
|
|
@@ -80,7 +73,7 @@ function sendToNode(request) {
|
|
|
80
73
|
if (Buffer.isBuffer(body))
|
|
81
74
|
headers['content-length'] = body.length;
|
|
82
75
|
return new Promise((resolve, reject) => {
|
|
83
|
-
const outgoing = lib.request(url, { method: request.method, headers, agent: proxyAgentFor(url), signal: request.signal }, resolve);
|
|
76
|
+
const outgoing = lib.request(url, { method: request.method, headers, agent: (0, outboundProxy_1.proxyAgentFor)(url), signal: request.signal }, resolve);
|
|
84
77
|
outgoing.on('error', reject);
|
|
85
78
|
if (body && !Buffer.isBuffer(body)) {
|
|
86
79
|
body.on('error', (err) => outgoing.destroy(err));
|