@xenon-device-management/xenon 2.15.0 → 2.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/README.md +38 -26
  2. package/lib/package.json +2 -1
  3. package/lib/public/assets/{AnnotationOverlay-Btq3dZRI.js → AnnotationOverlay-2W4JvEyZ.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-C9OoAgg_.js → ApiKeyGate-GmckX_7r.js} +1 -1
  5. package/lib/public/assets/{DeviceMosaicView-DBC_xczB.js → DeviceMosaicView-DqODzkyo.js} +1 -1
  6. package/lib/public/assets/{EmptyState-DqWU2-vM.js → EmptyState-7hETKEWb.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-CveQ3xrA.js → FieldGroup-GOgssFEP.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-BfKDSfHz.js → FilterMenu-Yvf6RGMd.js} +1 -1
  9. package/lib/public/assets/{Menu-CtdCybzA.js → Menu-KrPnfdsf.js} +1 -1
  10. package/lib/public/assets/{Modal-Bsvj1tm2.js → Modal-C6WjZFeI.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CRofOiyx.js → RecordingPage-CSRIhZEh.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-jNQHb2IR.js → RecordingsPage-CB8ST3nE.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-C8HtTha-.js → SegmentedControl-C9o90KId.js} +1 -1
  14. package/lib/public/assets/{SettingCard-D8K1LZNo.js → SettingCard-CPm3w9l2.js} +1 -1
  15. package/lib/public/assets/{Table-zNTX92Pn.js → Table-DwIgERRt.js} +1 -1
  16. package/lib/public/assets/{activity-CJ8LwZq9.js → activity-DrLbHifn.js} +1 -1
  17. package/lib/public/assets/{ai-settings-CgxQZqrc.js → ai-settings-DU_Ls06P.js} +1 -1
  18. package/lib/public/assets/{api-keys-ECpo-zTo.js → api-keys-CWYdEzJ_.js} +1 -1
  19. package/lib/public/assets/{apps-BjGymyf3.js → apps-BqNVdln7.js} +1 -1
  20. package/lib/public/assets/{arrow-left-BWfCbeHP.js → arrow-left-BtB0_Jg_.js} +1 -1
  21. package/lib/public/assets/{arrow-right-DC7UGf_n.js → arrow-right-C8NhdtyN.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-BOjwybDk.js → arrow-up-right-gMg0ffvv.js} +1 -1
  23. package/lib/public/assets/{auth-shell-CxMTo9aH.js → auth-shell-CiYZ3N4M.js} +2 -2
  24. package/lib/public/assets/{builds-page-DOCm4HkA.js → builds-page-EaaqDmLN.js} +1 -1
  25. package/lib/public/assets/{button-Cg08-EMT.js → button-DDau4Axv.js} +1 -1
  26. package/lib/public/assets/{calendar-CA1ikLJs.js → calendar-DQ83B1ed.js} +1 -1
  27. package/lib/public/assets/{check-BMTaH72W.js → check-K1ovTQam.js} +1 -1
  28. package/lib/public/assets/{chevron-right-B2QOv9Nx.js → chevron-right-Biq00pMt.js} +1 -1
  29. package/lib/public/assets/{circle-check-CYXBoUqT.js → circle-check-CBv88cH7.js} +1 -1
  30. package/lib/public/assets/{circle-x-DTwnSAnl.js → circle-x-Bmltailn.js} +1 -1
  31. package/lib/public/assets/{clock-C8_HViQP.js → clock-BPrld-3h.js} +1 -1
  32. package/lib/public/assets/{copy-Be7LNMXV.js → copy-t0QB14JI.js} +1 -1
  33. package/lib/public/assets/{device-explorer-BOqtfGXz.js → device-explorer-BgGZ8AvV.js} +1 -1
  34. package/lib/public/assets/{download-B56lwxWP.js → download-BroHxNOh.js} +1 -1
  35. package/lib/public/assets/{forgot-password-CCU6bzLn.js → forgot-password-Bjmf49i6.js} +1 -1
  36. package/lib/public/assets/{h264Stream-C-OtmIuk.js → h264Stream-DeD__6wa.js} +1 -1
  37. package/lib/public/assets/{index-CfxsUslp.js → index-72oF89LJ.js} +1 -1
  38. package/lib/public/assets/{index-B9KQ6LsY.js → index-B0JeuZbg.js} +1 -1
  39. package/lib/public/assets/index-B0gQkEg0.js +219 -0
  40. package/lib/public/assets/{input-CZHFjbSg.js → input-DIc4MSC7.js} +1 -1
  41. package/lib/public/assets/{line-chart-Gl_Dz3X_.js → line-chart-BcWm2vW8.js} +1 -1
  42. package/lib/public/assets/{list-checks-CCgjeyGY.js → list-checks-B06IJJad.js} +1 -1
  43. package/lib/public/assets/{lock-CkOwgDcx.js → lock-pVLvnN30.js} +1 -1
  44. package/lib/public/assets/{login-D5cc4w7M.js → login-CN77vy--.js} +1 -1
  45. package/lib/public/assets/{maintenance-settings-t7qr3fvX.js → maintenance-settings-CSdzQSkY.js} +1 -1
  46. package/lib/public/assets/{monitor-DEJixKQO.js → monitor-BtrN5IkI.js} +1 -1
  47. package/lib/public/assets/{mouse-pointer-2-CtOrk4M8.js → mouse-pointer-2-CNsQfo6w.js} +1 -1
  48. package/lib/public/assets/{network-Dii9qdvk.js → network-BSfA8ZxG.js} +1 -1
  49. package/lib/public/assets/{overview-rolAACrO.js → overview-BfTebCK5.js} +1 -1
  50. package/lib/public/assets/{page-header-COQZxJfR.js → page-header-D8KUX6Qg.js} +1 -1
  51. package/lib/public/assets/{play-DrN1Se_R.js → play-Cvc8xzic.js} +1 -1
  52. package/lib/public/assets/{plus-5aq-9uRx.js → plus-C9Zj84UF.js} +1 -1
  53. package/lib/public/assets/{profile-page-6i5kY6kF.js → profile-page-Bi27STgs.js} +1 -1
  54. package/lib/public/assets/{reset-password-sp2IBrc7.js → reset-password-CPsOuwSu.js} +1 -1
  55. package/lib/public/assets/{runbook-page-DJDjeopU.js → runbook-page-DEQ-DBOX.js} +153 -102
  56. package/lib/public/assets/{select-Bpyn7rWO.js → select-BWpnhWXj.js} +1 -1
  57. package/lib/public/assets/{selector-detail-redirect-BH74vmKv.js → selector-detail-redirect-BefSCSpe.js} +1 -1
  58. package/lib/public/assets/{selector-health-page-Cw_g218H.js → selector-health-page-MSk96Zz7.js} +1 -1
  59. package/lib/public/assets/session-detail-page-DPyBNef_.js +4 -0
  60. package/lib/public/assets/{settings-C3uyGhyc.js → settings-V6x-cXwy.js} +1 -1
  61. package/lib/public/assets/{stat-tile-xVqAKIlI.js → stat-tile-Duex-GZD.js} +1 -1
  62. package/lib/public/assets/{tablet-BxUScAeO.js → tablet-BBcoqBmL.js} +1 -1
  63. package/lib/public/assets/{teams-B5ljGkY2.js → teams-ts8p5rEh.js} +1 -1
  64. package/lib/public/assets/{trash-2-B701i8sy.js → trash-2-CZ2ew6AZ.js} +1 -1
  65. package/lib/public/assets/{upload-C2gr44RS.js → upload-Dv99y_le.js} +1 -1
  66. package/lib/public/assets/{use-builds-data-NeQOFEoy.js → use-builds-data-DrYC8JCF.js} +1 -1
  67. package/lib/public/assets/{use-password-reset-mode-DEzJq5ef.js → use-password-reset-mode-C7QkBNkP.js} +1 -1
  68. package/lib/public/assets/{users-D_vv_yz4.js → users-D7tKMESH.js} +1 -1
  69. package/lib/public/assets/{users-MY6eNe6s.js → users-gJ5Y01yo.js} +1 -1
  70. package/lib/public/assets/{video-off-OK1lNZQn.js → video-off-CqZVuZGs.js} +1 -1
  71. package/lib/public/assets/{webhook-settings-BU-C9lBf.js → webhook-settings-DI5DSW99.js} +1 -1
  72. package/lib/public/assets/{zap-DO9ry1iz.js → zap-ClcHFxfo.js} +1 -1
  73. package/lib/public/index.html +1 -1
  74. package/lib/schema.json +11 -7
  75. package/lib/src/InternalHttpClient.js +27 -5
  76. package/lib/src/XenonCapabilityManager.js +10 -3
  77. package/lib/src/app/openapi/sessions.yaml +7 -5
  78. package/lib/src/app/routers/auth.js +2 -1
  79. package/lib/src/app/routers/users.js +2 -1
  80. package/lib/src/app/ws/nodeSocketRelay.js +77 -11
  81. package/lib/src/dashboard/commandLogFields.js +42 -0
  82. package/lib/src/dashboard/event-manager.js +26 -154
  83. package/lib/src/dashboard/services/failure-analysis-service.js +8 -86
  84. package/lib/src/dashboard/services/failureCategories.js +264 -0
  85. package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
  86. package/lib/src/device-managers/IOSDeviceManager.js +10 -0
  87. package/lib/src/device-utils.js +14 -6
  88. package/lib/src/gateway/forwardToNode.js +4 -11
  89. package/lib/src/gateway/hubSessionToken.js +27 -4
  90. package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
  91. package/lib/src/helpers/outboundProxy.js +219 -0
  92. package/lib/src/index.js +21 -0
  93. package/lib/src/interceptors/CommandInterceptor.js +13 -15
  94. package/lib/src/interceptors/xenonScripts.js +28 -7
  95. package/lib/src/logger.js +3 -1
  96. package/lib/src/plugin.js +8 -3
  97. package/lib/src/scripts/database-check.js +3 -3
  98. package/lib/src/scripts/initialize-database.js +35 -33
  99. package/lib/src/scripts/run-migrations.js +1079 -27
  100. package/lib/src/services/ApiKeyService.js +16 -5
  101. package/lib/src/services/InterceptorService.js +6 -2
  102. package/lib/src/services/OrphanSweeper.js +6 -0
  103. package/lib/src/services/SelectorStateService.js +11 -7
  104. package/lib/src/services/SelectorVerificationJob.js +6 -2
  105. package/lib/src/services/SessionLifecycleService.js +32 -18
  106. package/lib/src/services/SocketClient.js +11 -0
  107. package/lib/src/services/SocketServer.js +361 -42
  108. package/lib/src/services/TeamService.js +5 -1
  109. package/lib/src/services/TracingService.js +171 -60
  110. package/lib/src/services/UserService.js +14 -3
  111. package/lib/src/services/UserSessionService.js +16 -4
  112. package/lib/src/services/healing/HealingOrchestrator.js +3 -1
  113. package/lib/src/services/identity/identityChanges.js +41 -0
  114. package/lib/src/services/logcat/SessionDeviceLogs.js +97 -10
  115. package/lib/src/services/logcat/deviceLogBook.js +5 -4
  116. package/lib/src/services/logcat/iosDriverLog.js +178 -0
  117. package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
  118. package/lib/src/services/token/JwtKeyService.js +8 -2
  119. package/lib/src/services/token/mcpTokenTtl.js +27 -0
  120. package/lib/src/sessions/LocalSession.js +29 -1
  121. package/lib/src/sessions/RemoteSession.js +12 -2
  122. package/lib/src/sessions/SessionManager.js +5 -8
  123. package/lib/test/helpers/driver-log.js +38 -0
  124. package/lib/test/helpers/fake-http-proxy.js +106 -0
  125. package/lib/test/helpers/otlp-probe.js +107 -0
  126. package/lib/test/integration/androidDevices.spec.js +1 -1
  127. package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
  128. package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
  129. package/lib/test/integration/selector-state-flow.spec.js +5 -5
  130. package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
  131. package/lib/test/unit/android-adb-retry.spec.js +112 -0
  132. package/lib/test/unit/command-log-network-capture.spec.js +191 -0
  133. package/lib/test/unit/dashboard-events-device-logs.spec.js +59 -11
  134. package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
  135. package/lib/test/unit/database-check.spec.js +1 -0
  136. package/lib/test/unit/device-utils.spec.js +13 -4
  137. package/lib/test/unit/event-log-session-data.spec.js +11 -4
  138. package/lib/test/unit/failure-analysis.spec.js +22 -8
  139. package/lib/test/unit/failure-categories.spec.js +24 -22
  140. package/lib/test/unit/failure-categorization.spec.js +393 -0
  141. package/lib/test/unit/healing-tier-gate.spec.js +3 -2
  142. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +72 -1
  143. package/lib/test/unit/ios-driver-log.spec.js +168 -0
  144. package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
  145. package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
  146. package/lib/test/unit/logcat-ws.spec.js +78 -37
  147. package/lib/test/unit/logger-json-levels.spec.js +44 -0
  148. package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
  149. package/lib/test/unit/observability-example-loki.spec.js +25 -0
  150. package/lib/test/unit/openapi-for-site.spec.js +80 -0
  151. package/lib/test/unit/outbound-proxy.spec.js +320 -0
  152. package/lib/test/unit/port-exhaustion.spec.js +2 -1
  153. package/lib/test/unit/proxy-option.spec.js +358 -0
  154. package/lib/test/unit/run-migrations-database.spec.js +807 -0
  155. package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
  156. package/lib/test/unit/run-migrations.test.js +864 -0
  157. package/lib/test/unit/selector-health-actions.spec.js +3 -1
  158. package/lib/test/unit/selector-state-service.spec.js +72 -23
  159. package/lib/test/unit/selector-verification-clean-build.spec.js +1 -1
  160. package/lib/test/unit/selector-verification-job.spec.js +15 -6
  161. package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
  162. package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
  163. package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
  164. package/lib/test/unit/session-device-logs.spec.js +11 -2
  165. package/lib/test/unit/session-end-failure-analysis.spec.js +5 -5
  166. package/lib/test/unit/session-log-indexes.spec.js +1 -1
  167. package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
  168. package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
  169. package/lib/test/unit/tracing-export-routing.spec.js +137 -0
  170. package/lib/test/unit/tracing-session-spans.spec.js +337 -0
  171. package/lib/test/unit/unexpected-shutdown-reason.spec.js +2 -2
  172. package/lib/tsconfig.tsbuildinfo +1 -1
  173. package/package.json +2 -1
  174. package/schema.json +11 -7
  175. package/scripts/dev/readme-screenshots.js +276 -23
  176. package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
  177. package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
  178. package/scripts/dev/screenshot-fixtures/session.js +242 -0
  179. package/scripts/export-openapi.js +32 -0
  180. package/scripts/lib/openapi-for-site.js +79 -0
  181. package/lib/public/assets/index-BOxe43p1.js +0 -219
  182. package/lib/public/assets/session-detail-page-AZJE6XXE.js +0 -4
  183. package/lib/src/scripts/prepare-prisma.js +0 -89
@@ -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
- logger_1.default.info('adb is not available. So, returning empty list');
165
- return [];
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
- try {
405
- if (!this.adb) {
406
- try {
407
- this.adb = yield appium_adb_1.ADB.createADB({});
408
- }
409
- catch (e) {
410
- this.adbAvailable = false;
411
- this.log.error('Could not find ADB');
412
- }
413
- if (process.env.NODE_ENV !== 'test') {
414
- const client = adbkit_1.default.createClient();
415
- this.tracker = yield client.trackDevices();
416
- if (this.tracker && this.adb) {
417
- const originalADBTracking = this.createLocalAdbTracker(this.tracker, this.adb);
418
- yield originalADBTracking();
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
- catch (e) {
424
- logger_1.default.error(`Failed to initialize ADB: ${e}`);
425
- this.adbAvailable = false;
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);
@@ -87,6 +87,7 @@ const device_managers_1 = require("./device-managers");
87
87
  const device_service_1 = require("./data-service/device-service");
88
88
  const deviceClaims_1 = require("./data-service/deviceClaims");
89
89
  const PluginContext_1 = require("./PluginContext");
90
+ const TracingService_1 = require("./services/TracingService");
90
91
  const logger_1 = __importDefault(require("./logger"));
91
92
  const Platform_1 = __importDefault(require("./enums/Platform"));
92
93
  const lodash_1 = __importDefault(require("lodash"));
@@ -250,7 +251,7 @@ function allocateDeviceForSession(capability_1, deviceTimeOutMs_1, deviceQueryIn
250
251
  if (device !== null)
251
252
  return true;
252
253
  }
253
- logger_1.default.info(`Waiting for free device. Filter: ${JSON.stringify(filters)}}`);
254
+ logger_1.default.info(`Waiting for free device. Filter: ${JSON.stringify(filters)}`);
254
255
  return false;
255
256
  }));
256
257
  }), { timeout, intervalBetweenAttempts });
@@ -269,7 +270,7 @@ function allocateDeviceForSession(capability_1, deviceTimeOutMs_1, deviceQueryIn
269
270
  else if (possibleDevice && (0, device_service_1.isDeviceReserved)(possibleDevice)) {
270
271
  failureReason = `Device is reserved by ${possibleDevice.reservedBy}.`;
271
272
  }
272
- throw new Error(`${failureReason}. Device request: ${JSON.stringify(filterCopy)}`);
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) {
@@ -648,7 +649,7 @@ function unblockCandidateDevices() {
648
649
  }
649
650
  function releaseBlockedDevices(newCommandTimeout) {
650
651
  return __awaiter(this, void 0, void 0, function* () {
651
- var _a, _b, _c;
652
+ var _a, _b;
652
653
  const busyDevices = yield unblockCandidateDevices();
653
654
  // A phone an SDK lease holds is the lease's to time out (its heartbeats and
654
655
  // expiry, LeaseOrphanSweeper), not idle until a session runs on it. A lease
@@ -696,9 +697,16 @@ function releaseBlockedDevices(newCommandTimeout) {
696
697
  if (timeSinceLastCmdExecuted > timeoutSeconds) {
697
698
  // unblock regardless of whether the device has session or not
698
699
  logger_1.default.info(`Unblocking device ${device.udid} at host ${device.host} because it has been idle for ${timeSinceLastCmdExecuted} seconds`);
700
+ const idleSession = (_a = device.claimSessionId) !== null && _a !== void 0 ? _a : device.session_id;
699
701
  // Before anything below can release the phone (onSessionStopped too).
700
- yield restoreIdleSessionNetwork((_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);
702
+ yield restoreIdleSessionNetwork(idleSession);
703
+ yield forgetIdleSessionMemory(idleSession);
704
+ if (idleSession) {
705
+ typedi_1.Container.get(TracingService_1.TracingService).endSessionSpan(idleSession, {
706
+ failed: true,
707
+ reason: 'Session timed out due to inactivity',
708
+ });
709
+ }
702
710
  // Principal Protection: If this device has an active dashboard session, stop it properly
703
711
  if (device.session_id) {
704
712
  const sessionId = device.session_id;
@@ -714,7 +722,7 @@ function releaseBlockedDevices(newCommandTimeout) {
714
722
  }
715
723
  // Keyed on the session read here, so a release that loses the race to
716
724
  // the session's own end (and the phone's next claim) frees nothing.
717
- const claimant = (_c = device.claimSessionId) !== null && _c !== void 0 ? _c : device.session_id;
725
+ const claimant = (_b = device.claimSessionId) !== null && _b !== void 0 ? _b : device.session_id;
718
726
  yield (claimant
719
727
  ? (0, device_service_1.releaseSessionDevice)(device.udid, device.host, claimant)
720
728
  : (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));
@@ -59,6 +59,7 @@ const jose = __importStar(require("jose"));
59
59
  const logger_1 = __importDefault(require("../logger"));
60
60
  const JwtKeyService_1 = require("../services/token/JwtKeyService");
61
61
  const singleUseLedger_1 = require("../services/token/singleUseLedger");
62
+ const outboundProxy_1 = require("../helpers/outboundProxy");
62
63
  /**
63
64
  * The credential a hub's calls to a node carry.
64
65
  *
@@ -221,6 +222,22 @@ exports.HubSessionTokenIssuer = HubSessionTokenIssuer = __decorate([
221
222
  function hubJwksUrl(hub) {
222
223
  return new URL('/xenon/api/auth/jwks.json', hub);
223
224
  }
225
+ /**
226
+ * A key lookup that fails after `ms`. jose's own timeout starts once its
227
+ * request has a socket, which a proxy agent hands it only after the proxy
228
+ * answers, so a proxy that never answers held every token check.
229
+ */
230
+ function withinTime(getKey, ms) {
231
+ return (header, token) => {
232
+ let timer;
233
+ return Promise.race([
234
+ Promise.resolve(getKey(header, token)),
235
+ new Promise((_, reject) => {
236
+ timer = setTimeout(() => reject(new Error(`the hub's keys did not arrive in ${ms} ms`)), ms);
237
+ }),
238
+ ]).finally(() => clearTimeout(timer));
239
+ };
240
+ }
224
241
  /** The node could not check a hub token (the hub's keys could not be fetched). */
225
242
  class HubTokenUnavailableError extends Error {
226
243
  }
@@ -251,15 +268,21 @@ function isVerdict(err) {
251
268
  * a token names a key it does not have (a hub that rotated its key).
252
269
  */
253
270
  class HubSessionTokenVerifier {
254
- constructor(hubUrl, keys) {
271
+ constructor(hubUrl, keys, opts = {}) {
272
+ var _a;
255
273
  /** The create tokens already taken, by id, until each one expires. */
256
274
  this.usedCreates = new singleUseLedger_1.SingleUseLedger(exports.HUB_CREATE_TTL_SECONDS);
275
+ const jwksUrl = hubJwksUrl(hubUrl);
276
+ const timeoutMs = (_a = opts.timeoutMs) !== null && _a !== void 0 ? _a : 5000;
257
277
  this.keys =
258
- keys !== null && keys !== void 0 ? keys : jose.createRemoteJWKSet(hubJwksUrl(hubUrl), {
259
- timeoutDuration: 5000,
278
+ keys !== null && keys !== void 0 ? keys : withinTime(jose.createRemoteJWKSet(jwksUrl, {
279
+ timeoutDuration: timeoutMs,
260
280
  cooldownDuration: 30000,
261
281
  cacheMaxAge: 10 * 60000,
262
- });
282
+ // The proxy the node's axios calls to its hub take; jose's own
283
+ // fetch takes none.
284
+ agent: (0, outboundProxy_1.proxyAgentFor)(jwksUrl),
285
+ }), timeoutMs);
263
286
  }
264
287
  verify(token, sessionId) {
265
288
  return __awaiter(this, void 0, void 0, function* () {
@@ -30,6 +30,7 @@ const logger_1 = __importDefault(require("../logger"));
30
30
  const helpers_1 = require("../helpers");
31
31
  const appiumBasePath_1 = require("../app/appiumBasePath");
32
32
  const PluginContext_1 = require("../PluginContext");
33
+ const outboundProxy_1 = require("../helpers/outboundProxy");
33
34
  /**
34
35
  * Where a node's WebDriver API lives.
35
36
  *
@@ -95,11 +96,8 @@ let NodeBasePathResolver = class NodeBasePathResolver {
95
96
  let advertised;
96
97
  try {
97
98
  const rejectUnauthorized = (_a = typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs) === null || _a === void 0 ? void 0 : _a.tlsRejectUnauthorized;
98
- const response = yield axios_1.default.get(`${origin}${exports.WEBDRIVER_INFO_PATH}`, {
99
- timeout: exports.LOOKUP_TIMEOUT_MS,
100
- validateStatus: (status) => status === 200,
101
- httpsAgent: new https_1.default.Agent({ rejectUnauthorized: rejectUnauthorized !== false }),
102
- });
99
+ const url = `${origin}${exports.WEBDRIVER_INFO_PATH}`;
100
+ const response = yield axios_1.default.get(url, Object.assign({ timeout: exports.LOOKUP_TIMEOUT_MS, validateStatus: (status) => status === 200, httpsAgent: new https_1.default.Agent({ rejectUnauthorized: rejectUnauthorized !== false }) }, (0, outboundProxy_1.axiosProxyConfig)(url, { rejectUnauthorized: rejectUnauthorized !== false })));
103
101
  const basePath = (_b = response.data) === null || _b === void 0 ? void 0 : _b.basePath;
104
102
  if (typeof basePath === 'string')
105
103
  advertised = (0, appiumBasePath_1.normalizeBasePath)(basePath);