@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.
Files changed (206) hide show
  1. package/README.md +38 -26
  2. package/lib/package.json +2 -1
  3. package/lib/public/assets/{AnnotationOverlay-Btq3dZRI.js → AnnotationOverlay-CjmMCS0S.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-C9OoAgg_.js → ApiKeyGate-B4Ob0v3C.js} +1 -1
  5. package/lib/public/assets/{DeviceMosaicView-DBC_xczB.js → DeviceMosaicView-DTVOB_NO.js} +1 -1
  6. package/lib/public/assets/{EmptyState-DqWU2-vM.js → EmptyState-DcEG-oAi.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-CveQ3xrA.js → FieldGroup-CtnCDshE.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-BfKDSfHz.js → FilterMenu-BK0dE9cc.js} +1 -1
  9. package/lib/public/assets/{Menu-CtdCybzA.js → Menu-eKsmLpKS.js} +1 -1
  10. package/lib/public/assets/{Modal-Bsvj1tm2.js → Modal-DPwop3Lz.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CRofOiyx.js → RecordingPage-CfgSkZkB.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-jNQHb2IR.js → RecordingsPage-Bzl9pTUY.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-C8HtTha-.js → SegmentedControl-B_TZTrPm.js} +1 -1
  14. package/lib/public/assets/{SettingCard-D8K1LZNo.js → SettingCard-DlmvZJPM.js} +1 -1
  15. package/lib/public/assets/{Table-zNTX92Pn.js → Table-BigPWKXi.js} +1 -1
  16. package/lib/public/assets/{activity-CJ8LwZq9.js → activity-CvuZdQYu.js} +1 -1
  17. package/lib/public/assets/{ai-settings-CgxQZqrc.js → ai-settings-lI2UJ2n7.js} +1 -1
  18. package/lib/public/assets/{api-keys-ECpo-zTo.js → api-keys-CPgf9XJM.js} +1 -1
  19. package/lib/public/assets/{apps-BjGymyf3.js → apps-DIzOvtUp.js} +1 -1
  20. package/lib/public/assets/{arrow-left-BWfCbeHP.js → arrow-left-DiP5ywBT.js} +1 -1
  21. package/lib/public/assets/{arrow-right-DC7UGf_n.js → arrow-right-DtW_8hm5.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-BOjwybDk.js → arrow-up-right-C89FbNU0.js} +1 -1
  23. package/lib/public/assets/{auth-shell-CxMTo9aH.js → auth-shell-B7sUDb--.js} +2 -2
  24. package/lib/public/assets/{builds-page-DOCm4HkA.js → builds-page-BBhvrMfA.js} +1 -1
  25. package/lib/public/assets/{button-Cg08-EMT.js → button-DugsdUw1.js} +1 -1
  26. package/lib/public/assets/{calendar-CA1ikLJs.js → calendar-grKd08ZH.js} +1 -1
  27. package/lib/public/assets/{check-BMTaH72W.js → check-ORjyNfnX.js} +1 -1
  28. package/lib/public/assets/{chevron-right-B2QOv9Nx.js → chevron-right-Ei2FxUXq.js} +1 -1
  29. package/lib/public/assets/{circle-check-CYXBoUqT.js → circle-check-Day0HjW4.js} +1 -1
  30. package/lib/public/assets/{circle-x-DTwnSAnl.js → circle-x-CHxSRLHp.js} +1 -1
  31. package/lib/public/assets/{clock-C8_HViQP.js → clock-BDHewWTx.js} +1 -1
  32. package/lib/public/assets/{copy-Be7LNMXV.js → copy-CTwT4I3D.js} +1 -1
  33. package/lib/public/assets/{device-explorer-BOqtfGXz.js → device-explorer-DlhrKJ-g.js} +1 -1
  34. package/lib/public/assets/{download-B56lwxWP.js → download-D3bjz_vr.js} +1 -1
  35. package/lib/public/assets/{forgot-password-CCU6bzLn.js → forgot-password-BNBLi023.js} +1 -1
  36. package/lib/public/assets/{h264Stream-C-OtmIuk.js → h264Stream-C51Au2i_.js} +1 -1
  37. package/lib/public/assets/index-Dk5J6HB1.js +219 -0
  38. package/lib/public/assets/{index-B9KQ6LsY.js → index-DncSz5LG.js} +1 -1
  39. package/lib/public/assets/{index-CfxsUslp.js → index-Z7_BY3pJ.js} +1 -1
  40. package/lib/public/assets/{input-CZHFjbSg.js → input-bTXM4l09.js} +1 -1
  41. package/lib/public/assets/{line-chart-Gl_Dz3X_.js → line-chart-B3YmVYyr.js} +1 -1
  42. package/lib/public/assets/{list-checks-CCgjeyGY.js → list-checks-BClKEj1-.js} +1 -1
  43. package/lib/public/assets/{lock-CkOwgDcx.js → lock-CZsenX3D.js} +1 -1
  44. package/lib/public/assets/{login-D5cc4w7M.js → login-B6A6tOp9.js} +1 -1
  45. package/lib/public/assets/{maintenance-settings-t7qr3fvX.js → maintenance-settings-CmCJGy7O.js} +1 -1
  46. package/lib/public/assets/{monitor-DEJixKQO.js → monitor-BdTNEglU.js} +1 -1
  47. package/lib/public/assets/{mouse-pointer-2-CtOrk4M8.js → mouse-pointer-2-BM8CaLtt.js} +1 -1
  48. package/lib/public/assets/{network-Dii9qdvk.js → network-BS6vUc1x.js} +1 -1
  49. package/lib/public/assets/{overview-rolAACrO.js → overview-Yfk_Y7g3.js} +1 -1
  50. package/lib/public/assets/{page-header-COQZxJfR.js → page-header-DhXCwpml.js} +1 -1
  51. package/lib/public/assets/{play-DrN1Se_R.js → play-D08gP1td.js} +1 -1
  52. package/lib/public/assets/{plus-5aq-9uRx.js → plus-BKboorfa.js} +1 -1
  53. package/lib/public/assets/{profile-page-6i5kY6kF.js → profile-page-Cd9NodrA.js} +1 -1
  54. package/lib/public/assets/{reset-password-sp2IBrc7.js → reset-password-acLDyyDW.js} +1 -1
  55. package/lib/public/assets/{runbook-page-DJDjeopU.js → runbook-page-CWLkC1Fd.js} +153 -102
  56. package/lib/public/assets/{select-Bpyn7rWO.js → select-DZoRKNti.js} +1 -1
  57. package/lib/public/assets/{selector-detail-redirect-BH74vmKv.js → selector-detail-redirect-DS4qrt84.js} +1 -1
  58. package/lib/public/assets/{selector-health-page-Cw_g218H.js → selector-health-page-TpmN61G3.js} +1 -1
  59. package/lib/public/assets/session-detail-page-C0wYaXqy.js +4 -0
  60. package/lib/public/assets/{settings-C3uyGhyc.js → settings-CxPGEY2j.js} +1 -1
  61. package/lib/public/assets/{stat-tile-xVqAKIlI.js → stat-tile-DUpyYgl-.js} +1 -1
  62. package/lib/public/assets/{tablet-BxUScAeO.js → tablet-SA02BJNQ.js} +1 -1
  63. package/lib/public/assets/{teams-B5ljGkY2.js → teams-jamD0Kxr.js} +1 -1
  64. package/lib/public/assets/{trash-2-B701i8sy.js → trash-2-DO8fv6w4.js} +1 -1
  65. package/lib/public/assets/{upload-C2gr44RS.js → upload-CaiGCStb.js} +1 -1
  66. package/lib/public/assets/{use-builds-data-NeQOFEoy.js → use-builds-data-jS-fcvVf.js} +1 -1
  67. package/lib/public/assets/{use-password-reset-mode-DEzJq5ef.js → use-password-reset-mode-BzoenegM.js} +1 -1
  68. package/lib/public/assets/{users-MY6eNe6s.js → users-Bnw7Vs_B.js} +1 -1
  69. package/lib/public/assets/{users-D_vv_yz4.js → users-DHvDrriG.js} +1 -1
  70. package/lib/public/assets/{video-off-OK1lNZQn.js → video-off-FEvy0w5U.js} +1 -1
  71. package/lib/public/assets/{webhook-settings-BU-C9lBf.js → webhook-settings-CBVB5lxi.js} +1 -1
  72. package/lib/public/assets/{zap-DO9ry1iz.js → zap-BMrTJO6M.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/index.js +2 -0
  78. package/lib/src/app/openapi/grid.yaml +102 -0
  79. package/lib/src/app/openapi/sessions.yaml +7 -5
  80. package/lib/src/app/routers/auth.js +2 -1
  81. package/lib/src/app/routers/users.js +2 -1
  82. package/lib/src/app/ws/nodeSocketRelay.js +77 -11
  83. package/lib/src/dashboard/commandLogFields.js +42 -0
  84. package/lib/src/dashboard/event-manager.js +23 -154
  85. package/lib/src/dashboard/services/failure-analysis-service.js +8 -86
  86. package/lib/src/dashboard/services/failureCategories.js +264 -0
  87. package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
  88. package/lib/src/device-managers/IOSDeviceManager.js +10 -0
  89. package/lib/src/device-utils.js +25 -8
  90. package/lib/src/gateway/forwardToNode.js +4 -11
  91. package/lib/src/gateway/hubSessionToken.js +27 -4
  92. package/lib/src/gateway/nodeAsk.js +57 -0
  93. package/lib/src/gateway/nodeSessionDeviceLogs.js +35 -0
  94. package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
  95. package/lib/src/helpers/outboundProxy.js +219 -0
  96. package/lib/src/index.js +21 -0
  97. package/lib/src/interceptors/CommandInterceptor.js +13 -15
  98. package/lib/src/interceptors/xenonScripts.js +28 -7
  99. package/lib/src/logger.js +3 -1
  100. package/lib/src/plugin.js +12 -4
  101. package/lib/src/scripts/database-check.js +3 -3
  102. package/lib/src/scripts/initialize-database.js +29 -33
  103. package/lib/src/scripts/run-migrations.js +320 -25
  104. package/lib/src/services/ApiKeyService.js +16 -5
  105. package/lib/src/services/InterceptorService.js +6 -2
  106. package/lib/src/services/OrphanSweeper.js +6 -0
  107. package/lib/src/services/SelectorStateService.js +11 -7
  108. package/lib/src/services/SelectorVerificationJob.js +6 -2
  109. package/lib/src/services/SessionLifecycleService.js +52 -23
  110. package/lib/src/services/SocketClient.js +11 -0
  111. package/lib/src/services/SocketServer.js +361 -42
  112. package/lib/src/services/TeamService.js +5 -1
  113. package/lib/src/services/TracingService.js +171 -60
  114. package/lib/src/services/UserService.js +14 -3
  115. package/lib/src/services/UserSessionService.js +16 -4
  116. package/lib/src/services/healing/HealingOrchestrator.js +3 -1
  117. package/lib/src/services/identity/identityChanges.js +41 -0
  118. package/lib/src/services/interceptor/CertManager.js +50 -16
  119. package/lib/src/services/logcat/NodeDeviceLogStore.js +99 -0
  120. package/lib/src/services/logcat/NodeDeviceLogsCollector.js +164 -0
  121. package/lib/src/services/logcat/SessionDeviceLogs.js +279 -37
  122. package/lib/src/services/logcat/deviceLogBook.js +5 -4
  123. package/lib/src/services/logcat/iosDriverLog.js +178 -0
  124. package/lib/src/services/logcat/nodeDeviceLogs.js +91 -0
  125. package/lib/src/services/metrics/nodeMetrics.js +20 -49
  126. package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
  127. package/lib/src/services/token/JwtKeyService.js +8 -2
  128. package/lib/src/services/token/mcpTokenTtl.js +27 -0
  129. package/lib/src/sessions/LocalSession.js +29 -1
  130. package/lib/src/sessions/RemoteSession.js +23 -4
  131. package/lib/src/sessions/SessionManager.js +33 -9
  132. package/lib/test/helpers/driver-log.js +38 -0
  133. package/lib/test/helpers/fake-http-proxy.js +106 -0
  134. package/lib/test/helpers/fake-logcat.js +80 -0
  135. package/lib/test/helpers/hub-node-pair.js +227 -0
  136. package/lib/test/helpers/otlp-probe.js +107 -0
  137. package/lib/test/integration/androidDevices.spec.js +1 -1
  138. package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
  139. package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
  140. package/lib/test/integration/selector-state-flow.spec.js +5 -5
  141. package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
  142. package/lib/test/unit/android-adb-retry.spec.js +112 -0
  143. package/lib/test/unit/command-log-network-capture.spec.js +191 -0
  144. package/lib/test/unit/dashboard-events-device-logs.spec.js +74 -11
  145. package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
  146. package/lib/test/unit/database-check.spec.js +1 -0
  147. package/lib/test/unit/device-utils.spec.js +13 -4
  148. package/lib/test/unit/event-log-session-data.spec.js +11 -4
  149. package/lib/test/unit/failure-analysis.spec.js +22 -8
  150. package/lib/test/unit/failure-categories.spec.js +24 -22
  151. package/lib/test/unit/failure-categorization.spec.js +393 -0
  152. package/lib/test/unit/healing-tier-gate.spec.js +3 -2
  153. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +72 -1
  154. package/lib/test/unit/hub-node-iphone-readiness.spec.js +134 -0
  155. package/lib/test/unit/hub-node-session-device-logs.spec.js +194 -0
  156. package/lib/test/unit/hub-node-session-metrics.spec.js +24 -192
  157. package/lib/test/unit/hub-remote-session-heartbeat.spec.js +8 -1
  158. package/lib/test/unit/interceptor/CertManager.spec.js +89 -0
  159. package/lib/test/unit/ios-driver-log.spec.js +168 -0
  160. package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
  161. package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
  162. package/lib/test/unit/logcat-ws.spec.js +78 -37
  163. package/lib/test/unit/logger-json-levels.spec.js +44 -0
  164. package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
  165. package/lib/test/unit/node-device-log-store.spec.js +102 -0
  166. package/lib/test/unit/node-device-logs-collector.spec.js +343 -0
  167. package/lib/test/unit/node-gateway-create-appium-server.spec.js +8 -1
  168. package/lib/test/unit/node-hub-session-owner.spec.js +8 -1
  169. package/lib/test/unit/node-session-device-logs-route.spec.js +103 -0
  170. package/lib/test/unit/observability-example-loki.spec.js +25 -0
  171. package/lib/test/unit/openapi-for-site.spec.js +80 -0
  172. package/lib/test/unit/outbound-proxy.spec.js +320 -0
  173. package/lib/test/unit/port-exhaustion.spec.js +2 -1
  174. package/lib/test/unit/proxy-option.spec.js +358 -0
  175. package/lib/test/unit/run-migrations-database.spec.js +440 -0
  176. package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
  177. package/lib/test/unit/run-migrations.test.js +264 -0
  178. package/lib/test/unit/selector-health-actions.spec.js +3 -1
  179. package/lib/test/unit/selector-state-service.spec.js +72 -23
  180. package/lib/test/unit/selector-verification-clean-build.spec.js +1 -1
  181. package/lib/test/unit/selector-verification-job.spec.js +15 -6
  182. package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
  183. package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
  184. package/lib/test/unit/session-device-logs-hub-node.spec.js +340 -0
  185. package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
  186. package/lib/test/unit/session-device-logs-lifecycle.spec.js +137 -0
  187. package/lib/test/unit/session-device-logs.spec.js +43 -96
  188. package/lib/test/unit/session-end-failure-analysis.spec.js +5 -5
  189. package/lib/test/unit/session-log-indexes.spec.js +16 -1
  190. package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
  191. package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
  192. package/lib/test/unit/tracing-export-routing.spec.js +137 -0
  193. package/lib/test/unit/tracing-session-spans.spec.js +337 -0
  194. package/lib/test/unit/unexpected-shutdown-reason.spec.js +2 -2
  195. package/lib/tsconfig.tsbuildinfo +1 -1
  196. package/package.json +2 -1
  197. package/schema.json +11 -7
  198. package/scripts/dev/readme-screenshots.js +276 -23
  199. package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
  200. package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
  201. package/scripts/dev/screenshot-fixtures/session.js +242 -0
  202. package/scripts/export-openapi.js +32 -0
  203. package/scripts/lib/openapi-for-site.js +79 -0
  204. package/lib/public/assets/index-BOxe43p1.js +0 -219
  205. package/lib/public/assets/session-detail-page-AZJE6XXE.js +0 -4
  206. package/lib/src/scripts/prepare-prisma.js +0 -89
@@ -18,7 +18,9 @@ const typedi_1 = require("typedi");
18
18
  const logger_1 = __importDefault(require("../logger"));
19
19
  const SessionType_1 = __importDefault(require("../enums/SessionType"));
20
20
  const hubSessionToken_1 = require("../gateway/hubSessionToken");
21
+ const outboundProxy_1 = require("../helpers/outboundProxy");
21
22
  const nodeMetrics_1 = require("../services/metrics/nodeMetrics");
23
+ const nodeDeviceLogs_1 = require("../services/logcat/nodeDeviceLogs");
22
24
  const nodeSessionStatus_1 = require("../gateway/nodeSessionStatus");
23
25
  const XenonSession_1 = require("./XenonSession");
24
26
  class RemoteSession extends XenonSession_1.XenonSession {
@@ -52,7 +54,8 @@ class RemoteSession extends XenonSession_1.XenonSession {
52
54
  return __awaiter(this, void 0, void 0, function* () {
53
55
  var _a, _b;
54
56
  const options = yield this.callOptions();
55
- return (0, axios_1.default)(Object.assign(Object.assign(Object.assign({}, config), options), { headers: Object.assign(Object.assign({}, ((_a = config.headers) !== null && _a !== void 0 ? _a : {})), ((_b = options.headers) !== null && _b !== void 0 ? _b : {})) }));
57
+ const request = Object.assign(Object.assign({}, config), options);
58
+ return (0, axios_1.default)(Object.assign(Object.assign(Object.assign({}, (request.proxy === undefined ? proxyFor(config.url) : {})), request), { headers: Object.assign(Object.assign({}, ((_a = config.headers) !== null && _a !== void 0 ? _a : {})), ((_b = options.headers) !== null && _b !== void 0 ? _b : {})) }));
56
59
  });
57
60
  }
58
61
  getScreenShot() {
@@ -233,6 +236,14 @@ class RemoteSession extends XenonSession_1.XenonSession {
233
236
  }
234
237
  /** The node's CPU and memory for this session newer than `after` (NodeMetricsCollector). */
235
238
  nodeMetrics(after) {
239
+ return this.askNode('metrics', after, nodeMetrics_1.readNodeMetricsReply);
240
+ }
241
+ /** The node's device log lines for this session after line `after` (NodeDeviceLogsCollector). */
242
+ nodeDeviceLogs(after) {
243
+ return this.askNode('device-logs', after, nodeDeviceLogs_1.readNodeDeviceLogsReply);
244
+ }
245
+ /** One of the node's routes about this session, `/xenon/api/node/sessions/<id>/<what>?after=`. */
246
+ askNode(what, after, read) {
236
247
  return __awaiter(this, void 0, void 0, function* () {
237
248
  var _a, _b, _c;
238
249
  const origin = this.nodeOrigin();
@@ -241,12 +252,12 @@ class RemoteSession extends XenonSession_1.XenonSession {
241
252
  try {
242
253
  const response = yield this.call({
243
254
  method: 'get',
244
- url: `${origin}${nodeSessionStatus_1.NODE_SESSION_STATUS_PATH}/${encodeURIComponent(this.sessionId)}/metrics`,
255
+ url: `${origin}${nodeSessionStatus_1.NODE_SESSION_STATUS_PATH}/${encodeURIComponent(this.sessionId)}/${what}`,
245
256
  params: after === null ? undefined : { after },
246
257
  timeout: 5000,
247
258
  validateStatus: () => true,
248
259
  });
249
- return (0, nodeMetrics_1.readNodeMetricsReply)(response.status, (_a = response.headers) !== null && _a !== void 0 ? _a : {}, response.data);
260
+ return read(response.status, (_a = response.headers) !== null && _a !== void 0 ? _a : {}, response.data);
250
261
  }
251
262
  catch (err) {
252
263
  return { kind: 'unavailable', reason: String((_c = (_b = err === null || err === void 0 ? void 0 : err.code) !== null && _b !== void 0 ? _b : err === null || err === void 0 ? void 0 : err.message) !== null && _c !== void 0 ? _c : err) };
@@ -392,7 +403,7 @@ class RemoteSession extends XenonSession_1.XenonSession {
392
403
  logger_1.default.warn(`[RemoteSession] Session ${this.sessionId} probe returned 404. Performing server status fallback...`);
393
404
  // Fallback: Verify Appium server is alive
394
405
  try {
395
- const statusRes = yield axios_1.default.get(`${appiumUrl}/status`, { timeout: 3000 });
406
+ const statusRes = yield axios_1.default.get(`${appiumUrl}/status`, Object.assign({ timeout: 3000 }, proxyFor(`${appiumUrl}/status`)));
396
407
  if (statusRes.status === 200) {
397
408
  // If server is alive but we got a raw 404 with no specific error,
398
409
  // it's likely the session is gone in Appium 2 (where /sessions list check is unsupported).
@@ -438,3 +449,11 @@ class RemoteSession extends XenonSession_1.XenonSession {
438
449
  }
439
450
  }
440
451
  exports.RemoteSession = RemoteSession;
452
+ function proxyFor(url) {
453
+ try {
454
+ return url ? (0, outboundProxy_1.axiosProxyConfig)(url) : {};
455
+ }
456
+ catch (_a) {
457
+ return {};
458
+ }
459
+ }
@@ -51,7 +51,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
51
51
  return (mod && mod.__esModule) ? mod : { "default": mod };
52
52
  };
53
53
  Object.defineProperty(exports, "__esModule", { value: true });
54
- exports.SESSION_MANAGER = exports.SessionManager = exports.HUB_RESTART_CATEGORY = void 0;
54
+ exports.SESSION_MANAGER = exports.SessionManager = void 0;
55
55
  const os_1 = __importDefault(require("os"));
56
56
  const typedi_1 = require("typedi");
57
57
  const prisma_1 = require("../prisma");
@@ -59,15 +59,14 @@ const logger_1 = __importDefault(require("../logger"));
59
59
  const RemoteSession_1 = require("./RemoteSession");
60
60
  const CloudSession_1 = require("./CloudSession");
61
61
  const device_store_1 = require("../data-service/device-store");
62
+ const PluginContext_1 = require("../PluginContext");
62
63
  const nodeWebDriverUrl_1 = require("../gateway/nodeWebDriverUrl");
63
64
  const XenonCapabilityManager_1 = require("../XenonCapabilityManager");
64
65
  const nodeMetrics_1 = require("../services/metrics/nodeMetrics");
65
- /**
66
- * The `failure_category` of a session a restart ended: one still running at
67
- * boot that couldn't be picked up again. The failure analysis writes every
68
- * other category (`ANALYSIS_CATEGORIES`).
69
- */
70
- exports.HUB_RESTART_CATEGORY = 'HUB_RESTART';
66
+ const nodeDeviceLogs_1 = require("../services/logcat/nodeDeviceLogs");
67
+ // A session still running at boot that can't be picked up again is filed
68
+ // under it; the failure analysis also writes it for a shutdown's drain.
69
+ const failureCategories_1 = require("../dashboard/services/failureCategories");
71
70
  /**
72
71
  * SessionManager with persistence and recovery capabilities.
73
72
  *
@@ -124,7 +123,7 @@ let SessionManager = class SessionManager {
124
123
  */
125
124
  recoverActiveSessions(currentNodeId_1, nodeBasePath_1) {
126
125
  return __awaiter(this, arguments, void 0, function* (currentNodeId, nodeBasePath, isLocalHost = () => false) {
127
- var _a, _b;
126
+ var _a, _b, _c;
128
127
  this.log.info('🔄 Attempting to recover active sessions from database...');
129
128
  try {
130
129
  // Find all sessions that are still "running" (not ended)
@@ -228,6 +227,13 @@ let SessionManager = class SessionManager {
228
227
  if (!device.cloud && dbSession.is_profiling_available) {
229
228
  yield this.resumeNodeMetrics(dbSession.id, device, recoveredSession, sessionResponse);
230
229
  }
230
+ // Its device log too, where the hub records sessions (the dashboard
231
+ // on) and the session didn't turn it off, after the newest line stored.
232
+ if (!device.cloud &&
233
+ ((_c = typedi_2.Container.get(PluginContext_1.PluginContext).pluginArgs) === null || _c === void 0 ? void 0 : _c.enableDashboard) &&
234
+ xenonCapabilities[XenonCapabilityManager_1.XENON_CAPABILITIES.SAVE_DEVICE_LOGS] !== false) {
235
+ yield this.resumeNodeDeviceLogs(dbSession.id, device, recoveredSession);
236
+ }
231
237
  recoveredCount++;
232
238
  }
233
239
  catch (sessionErr) {
@@ -269,6 +275,24 @@ let SessionManager = class SessionManager {
269
275
  }
270
276
  });
271
277
  }
278
+ /** Goes on collecting a node session's device log after a hub restart. */
279
+ resumeNodeDeviceLogs(sessionId, device, session) {
280
+ return __awaiter(this, void 0, void 0, function* () {
281
+ try {
282
+ // Loaded here: SessionDeviceLogs imports the device managers.
283
+ const { SessionDeviceLogs } = yield Promise.resolve().then(() => __importStar(require('../services/logcat/SessionDeviceLogs')));
284
+ void typedi_2.Container.get(SessionDeviceLogs).start({
285
+ sessionId,
286
+ device,
287
+ source: (0, nodeDeviceLogs_1.nodeDeviceLogsSourceOf)(session),
288
+ resume: true,
289
+ });
290
+ }
291
+ catch (err) {
292
+ this.log.warn(`Session ${sessionId}: device log not resumed: ${err.message}`);
293
+ }
294
+ });
295
+ }
272
296
  /**
273
297
  * A recovered session is this process's from now on: its row's heartbeat
274
298
  * is stamped with this process, now. The row still carried the heartbeat
@@ -312,7 +336,7 @@ let SessionManager = class SessionManager {
312
336
  status: 'failed',
313
337
  endTime: new Date(),
314
338
  failure_reason: reason,
315
- failure_category: exports.HUB_RESTART_CATEGORY,
339
+ failure_category: failureCategories_1.HUB_RESTART_CATEGORY,
316
340
  },
317
341
  });
318
342
  }
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.TestDriverLog = void 0;
13
+ const line_consuming_log_1 = require("appium-xcuitest-driver/build/lib/device/log/line-consuming-log");
14
+ const quietLog = { debug() { }, info() { }, warn() { }, error() { } };
15
+ /**
16
+ * The XCUITest driver's own log class, as an iPhone's (`IOSDeviceLog`) and a
17
+ * simulator's (`IOSSimulatorLog`) `logs.syslog` extend it, with `line()` in
18
+ * place of a device: what Xenon reads is what the driver keeps.
19
+ */
20
+ class TestDriverLog extends line_consuming_log_1.LineConsumingLog {
21
+ constructor() {
22
+ super({ log: quietLog });
23
+ }
24
+ startCapture() {
25
+ return __awaiter(this, void 0, void 0, function* () { });
26
+ }
27
+ stopCapture() {
28
+ return __awaiter(this, void 0, void 0, function* () { });
29
+ }
30
+ get isCapturing() {
31
+ return true;
32
+ }
33
+ /** A line from the device, as the driver's capture hands it on. */
34
+ line(message) {
35
+ this.broadcast(message);
36
+ }
37
+ }
38
+ exports.TestDriverLog = TestDriverLog;
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.startFakeProxy = startFakeProxy;
16
+ const http_1 = __importDefault(require("http"));
17
+ const net_1 = __importDefault(require("net"));
18
+ /**
19
+ * An HTTP proxy on 127.0.0.1 that forwards what it is sent and notes it: a
20
+ * request in absolute form (http-proxy-agent, axios) is passed on to its
21
+ * target, a CONNECT (https-proxy-agent, so WebSockets) becomes a tunnel, or
22
+ * is refused with `refuseConnect`. A spec can then tell a call that went
23
+ * through the proxy from one that went straight to the target.
24
+ */
25
+ function startFakeProxy() {
26
+ return __awaiter(this, arguments, void 0, function* (opts = {}) {
27
+ const seen = [];
28
+ const authorizations = [];
29
+ const sockets = new Set();
30
+ const resolve = (host) => { var _a, _b; return (_b = (_a = opts.hosts) === null || _a === void 0 ? void 0 : _a[host]) !== null && _b !== void 0 ? _b : host; };
31
+ const server = http_1.default.createServer((req, res) => {
32
+ var _a, _b;
33
+ seen.push(`${req.method} ${req.url}`);
34
+ authorizations.push(String((_a = req.headers['proxy-authorization']) !== null && _a !== void 0 ? _a : ''));
35
+ let target;
36
+ try {
37
+ target = new URL((_b = req.url) !== null && _b !== void 0 ? _b : '');
38
+ }
39
+ catch (_c) {
40
+ res.writeHead(400).end();
41
+ return;
42
+ }
43
+ const upstream = http_1.default.request(Object.assign(Object.assign({}, urlParts(target)), { hostname: resolve(target.hostname), method: req.method, headers: req.headers }), (answer) => {
44
+ var _a;
45
+ res.writeHead((_a = answer.statusCode) !== null && _a !== void 0 ? _a : 502, answer.headers);
46
+ answer.pipe(res);
47
+ });
48
+ upstream.on('error', () => res.writeHead(502).end());
49
+ req.pipe(upstream);
50
+ });
51
+ server.on('connect', (req, client, head) => {
52
+ var _a;
53
+ seen.push(`CONNECT ${req.url}`);
54
+ authorizations.push(String((_a = req.headers['proxy-authorization']) !== null && _a !== void 0 ? _a : ''));
55
+ if (opts.ignoreConnect) {
56
+ sockets.add(client);
57
+ client.on('error', () => client.destroy());
58
+ return;
59
+ }
60
+ if (opts.refuseConnect) {
61
+ client.end('HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\n\r\n');
62
+ return;
63
+ }
64
+ const [host, port] = String(req.url).split(':');
65
+ const upstream = net_1.default.connect(Number(port), resolve(host), () => {
66
+ client.write('HTTP/1.1 200 Connection Established\r\n\r\n');
67
+ if (head.length)
68
+ upstream.write(head);
69
+ upstream.pipe(client);
70
+ client.pipe(upstream);
71
+ });
72
+ sockets.add(upstream).add(client);
73
+ const drop = () => {
74
+ upstream.destroy();
75
+ client.destroy();
76
+ };
77
+ upstream.on('error', drop);
78
+ client.on('error', drop);
79
+ });
80
+ yield new Promise((resolve, reject) => {
81
+ server.once('error', reject);
82
+ server.listen(0, '127.0.0.1', () => resolve());
83
+ });
84
+ const { port } = server.address();
85
+ return {
86
+ url: `http://127.0.0.1:${port}`,
87
+ seen,
88
+ authorizations,
89
+ close: () => new Promise((resolve) => {
90
+ var _a;
91
+ for (const socket of sockets)
92
+ socket.destroy();
93
+ (_a = server.closeAllConnections) === null || _a === void 0 ? void 0 : _a.call(server);
94
+ server.close(() => resolve());
95
+ }),
96
+ };
97
+ });
98
+ }
99
+ function urlParts(url) {
100
+ return {
101
+ protocol: url.protocol,
102
+ hostname: url.hostname,
103
+ port: url.port,
104
+ path: `${url.pathname}${url.search}`,
105
+ };
106
+ }
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.until = exports.settle = exports.FakeLogcatStreams = void 0;
13
+ exports.lineAt = lineAt;
14
+ exports.fakeProc = fakeProc;
15
+ const stream_1 = require("stream");
16
+ const LogcatStreamService_1 = require("../../src/device-managers/android/LogcatStreamService");
17
+ const PackageResolver_1 = require("../../src/services/logcat/PackageResolver");
18
+ const pad = (n, w = 2) => String(n).padStart(w, '0');
19
+ /** A threadtime line logged at `at` by this machine's local clock. */
20
+ function lineAt(at, msg, level = 'I', tag = 'Tag', pid = 4127) {
21
+ const t = new Date(at);
22
+ return (`${pad(t.getMonth() + 1)}-${pad(t.getDate())} ${pad(t.getHours())}:${pad(t.getMinutes())}:` +
23
+ `${pad(t.getSeconds())}.${pad(t.getMilliseconds(), 3)} ${pid} ${pid} ${level} ${tag}: ${msg}\n`);
24
+ }
25
+ /** An `adb logcat` child whose output the test writes. */
26
+ function fakeProc() {
27
+ const stdout = new stream_1.PassThrough();
28
+ const handlers = {};
29
+ return {
30
+ stdout,
31
+ killed: false,
32
+ on(ev, cb) {
33
+ (handlers[ev] || (handlers[ev] = [])).push(cb);
34
+ },
35
+ /** The process ends, as when the phone restarts or is unplugged. */
36
+ exit() {
37
+ stdout.end();
38
+ (handlers.close || []).forEach((h) => h());
39
+ },
40
+ kill() {
41
+ this.killed = true;
42
+ },
43
+ };
44
+ }
45
+ /** The real stream service, its `adb logcat` children fake. */
46
+ class FakeLogcatStreams extends LogcatStreamService_1.LogcatStreamService {
47
+ constructor() {
48
+ super(...arguments);
49
+ this.procs = [];
50
+ }
51
+ spawnLogcat() {
52
+ return __awaiter(this, void 0, void 0, function* () {
53
+ const proc = fakeProc();
54
+ this.procs.push(proc);
55
+ return proc;
56
+ });
57
+ }
58
+ makeResolver() {
59
+ return new PackageResolver_1.PackageResolver(() => __awaiter(this, void 0, void 0, function* () { return ' PID NAME\n 4127 com.example.app\n'; }));
60
+ }
61
+ get proc() {
62
+ return this.procs[this.procs.length - 1];
63
+ }
64
+ }
65
+ exports.FakeLogcatStreams = FakeLogcatStreams;
66
+ /** Lets readline, the stream's package lookups and the mux deliver what was written. */
67
+ const settle = () => __awaiter(void 0, void 0, void 0, function* () {
68
+ for (let i = 0; i < 5; i++)
69
+ yield new Promise((r) => setImmediate(r));
70
+ });
71
+ exports.settle = settle;
72
+ const until = (check_1, ...args_1) => __awaiter(void 0, [check_1, ...args_1], void 0, function* (check, ms = 2000) {
73
+ const end = Date.now() + ms;
74
+ while (!check()) {
75
+ if (Date.now() > end)
76
+ throw new Error('timed out');
77
+ yield new Promise((r) => setTimeout(r, 5));
78
+ }
79
+ });
80
+ exports.until = until;
@@ -0,0 +1,227 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.NODE_ID = void 0;
16
+ exports.useHubAndNode = useHubAndNode;
17
+ const chai_1 = require("chai");
18
+ const express_1 = __importDefault(require("express"));
19
+ const fs_1 = __importDefault(require("fs"));
20
+ const http_1 = __importDefault(require("http"));
21
+ const os_1 = __importDefault(require("os"));
22
+ const path_1 = __importDefault(require("path"));
23
+ const sinon_1 = __importDefault(require("sinon"));
24
+ const typedi_1 = require("typedi");
25
+ const loopbackRequest_1 = __importDefault(require("./loopbackRequest"));
26
+ const plugin_1 = require("../../src/plugin");
27
+ const registerCommandAuth_1 = require("../../src/app/registerCommandAuth");
28
+ const defaultGateway_1 = require("../../src/gateway/defaultGateway");
29
+ const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
30
+ const nodeSessionStatus_1 = require("../../src/gateway/nodeSessionStatus");
31
+ const nodeSessionMetrics_1 = require("../../src/gateway/nodeSessionMetrics");
32
+ const nodeSessionDeviceLogs_1 = require("../../src/gateway/nodeSessionDeviceLogs");
33
+ const JwtKeyService_1 = require("../../src/services/token/JwtKeyService");
34
+ const PluginContext_1 = require("../../src/PluginContext");
35
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
36
+ const device_managers_1 = require("../../src/device-managers");
37
+ const device_store_1 = require("../../src/data-service/device-store");
38
+ const prisma_store_1 = require("../../src/data-service/prisma-store");
39
+ const SessionManager_1 = require("../../src/sessions/SessionManager");
40
+ const RemoteSession_1 = require("../../src/sessions/RemoteSession");
41
+ const appiumUmbrella_1 = require("../../src/sessions/appiumUmbrella");
42
+ const config_1 = require("../../src/config");
43
+ const container_registration_1 = require("./container-registration");
44
+ const appium_umbrella_1 = require("./appium-umbrella");
45
+ exports.NODE_ID = 'node-1';
46
+ const quiet = { info: () => undefined, warn: () => undefined, error: () => undefined };
47
+ /**
48
+ * A hub and a node in one process, for what a hub collects from a node about
49
+ * a session it runs: the node in Appium 3's own server() (Appium's umbrella
50
+ * with Xenon's plugin, the production node gateway, and the node's
51
+ * session-status, metrics and device-log routes), with per-command auth on,
52
+ * so every ask carries the hub's session token. The hub serves only its
53
+ * public keys: the spec plays the hub's side through the session's
54
+ * RemoteSession.
55
+ *
56
+ * Call it inside a `describe`, after useScratchDatabase(). `services` are the
57
+ * container ids the spec replaces in its own `beforeEach`; they are put back
58
+ * after each test.
59
+ */
60
+ function useHubAndNode(scratch, services = []) {
61
+ const servers = [];
62
+ let dirs;
63
+ let hubKeys;
64
+ let appiumLogs;
65
+ let restore;
66
+ let saved;
67
+ let authDisabledBefore;
68
+ let commandAuthBefore;
69
+ let nodeOrigin = '';
70
+ before(() => __awaiter(this, void 0, void 0, function* () {
71
+ appiumLogs = (0, appium_umbrella_1.quietAppiumLogs)();
72
+ dirs = [fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-hub-node-keys-'))];
73
+ hubKeys = new JwtKeyService_1.JwtKeyService();
74
+ yield hubKeys.init(dirs[0]);
75
+ }));
76
+ after(() => {
77
+ appiumLogs.restore();
78
+ for (const dir of dirs)
79
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
80
+ });
81
+ beforeEach(() => __awaiter(this, void 0, void 0, function* () {
82
+ restore = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, hubSessionToken_1.HubSessionTokenIssuer, device_managers_1.XenonManager, appiumUmbrella_1.AppiumUmbrella, nodeSessionStatus_1.NodeSessionProbeSupport, ...services);
83
+ // One process plays both sides: the hub's keys sign, the node checks them.
84
+ typedi_1.Container.set(JwtKeyService_1.JwtKeyService, hubKeys);
85
+ typedi_1.Container.set(hubSessionToken_1.HubSessionTokenIssuer, new hubSessionToken_1.HubSessionTokenIssuer());
86
+ typedi_1.Container.set(appiumUmbrella_1.AppiumUmbrella, new appiumUmbrella_1.AppiumUmbrella());
87
+ typedi_1.Container.set(nodeSessionStatus_1.NodeSessionProbeSupport, new nodeSessionStatus_1.NodeSessionProbeSupport());
88
+ typedi_1.Container.set(device_managers_1.XenonManager, {
89
+ getMaxSessionCount: () => undefined,
90
+ deviceInstances: () => __awaiter(this, void 0, void 0, function* () { return []; }),
91
+ });
92
+ const context = typedi_1.Container.get(PluginContext_1.PluginContext);
93
+ saved = {
94
+ store: device_store_1.DeviceStoreFactory._deviceStore,
95
+ pending: device_store_1.DeviceStoreFactory._pendingSessionStore,
96
+ context: Object.assign({}, context),
97
+ };
98
+ device_store_1.DeviceStoreFactory._deviceStore = new prisma_store_1.PrismaDeviceStore();
99
+ device_store_1.DeviceStoreFactory._pendingSessionStore = new prisma_store_1.PrismaPendingSessionStore();
100
+ authDisabledBefore = config_1.config.authDisabled;
101
+ config_1.config.authDisabled = false;
102
+ // Per-command auth on: the node then takes the hub's token and nothing else.
103
+ commandAuthBefore = process.env.XENON_REQUIRE_COMMAND_AUTH;
104
+ process.env.XENON_REQUIRE_COMMAND_AUTH = '1';
105
+ yield scratch.db.pendingSession.deleteMany({});
106
+ yield scratch.db.device.deleteMany({});
107
+ }));
108
+ afterEach(() => __awaiter(this, void 0, void 0, function* () {
109
+ for (const s of SessionManager_1.SESSION_MANAGER.getAllSessions())
110
+ SessionManager_1.SESSION_MANAGER.removeSession(s.getId());
111
+ device_store_1.DeviceStoreFactory._deviceStore = saved.store;
112
+ device_store_1.DeviceStoreFactory._pendingSessionStore = saved.pending;
113
+ Object.assign(typedi_1.Container.get(PluginContext_1.PluginContext), saved.context);
114
+ config_1.config.authDisabled = authDisabledBefore;
115
+ if (commandAuthBefore === undefined)
116
+ delete process.env.XENON_REQUIRE_COMMAND_AUTH;
117
+ else
118
+ process.env.XENON_REQUIRE_COMMAND_AUTH = commandAuthBefore;
119
+ sinon_1.default.restore();
120
+ restore();
121
+ for (const s of servers.splice(0)) {
122
+ s.closeAllConnections();
123
+ yield new Promise((resolve) => http_1.default.Server.prototype.close.call(s, () => resolve()));
124
+ }
125
+ }));
126
+ const port = (s) => s.address().port;
127
+ function boot() {
128
+ return __awaiter(this, arguments, void 0, function* (nodeArgs = {}) {
129
+ const baseDriver = (0, appium_umbrella_1.appiumBaseDriver)();
130
+ const hubServer = yield baseDriver.server({
131
+ routeConfiguringFunction: baseDriver.routeConfiguringFunction((0, appium_umbrella_1.umbrellaWith)([])),
132
+ port: 0,
133
+ hostname: '127.0.0.1',
134
+ basePath: '/wd/hub',
135
+ cliArgs: { basePath: '/wd/hub' },
136
+ serverUpdaters: [
137
+ (app) => {
138
+ app.get('/xenon/api/auth/jwks.json', (_req, res) => res.json(hubKeys.jwks()));
139
+ },
140
+ ],
141
+ });
142
+ servers.push(hubServer);
143
+ const hubUrl = `http://127.0.0.1:${port(hubServer)}`;
144
+ const pluginArgs = Object.assign(Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { hub: hubUrl, bindHostOrIp: '127.0.0.1', enableDashboard: false, deviceAvailabilityTimeoutMs: 1000, deviceAvailabilityQueryIntervalMs: 100 }), nodeArgs);
145
+ const umbrella = (0, appium_umbrella_1.umbrellaWith)([[plugin_1.XenonPlugin, 'xenon']]);
146
+ const nodeServer = yield baseDriver.server({
147
+ routeConfiguringFunction: baseDriver.routeConfiguringFunction(umbrella),
148
+ port: 0,
149
+ hostname: '127.0.0.1',
150
+ basePath: '/node',
151
+ cliArgs: { basePath: '/node' },
152
+ serverUpdaters: [
153
+ (app) => {
154
+ (0, registerCommandAuth_1.registerSessionGateway)(app, { basePath: '/node' }, (0, registerCommandAuth_1.commandAuthDeps)({ enabled: () => true, authDisabled: () => false, logger: quiet }), (0, defaultGateway_1.nodeGatewayOptions)(hubUrl));
155
+ // What ServerManager mounts under /xenon/api on a node.
156
+ const api = express_1.default.Router();
157
+ (0, nodeSessionStatus_1.registerNodeSessionStatus)(api, pluginArgs);
158
+ (0, nodeSessionMetrics_1.registerNodeSessionMetrics)(api, pluginArgs);
159
+ (0, nodeSessionDeviceLogs_1.registerNodeSessionDeviceLogs)(api, pluginArgs);
160
+ app.use('/xenon/api', api);
161
+ },
162
+ ],
163
+ });
164
+ servers.push(nodeServer);
165
+ nodeOrigin = `http://127.0.0.1:${port(nodeServer)}`;
166
+ typedi_1.Container.get(PluginContext_1.PluginContext).setContext(pluginArgs, port(nodeServer), exports.NODE_ID, '/node');
167
+ yield scratch.db.device.create({
168
+ data: {
169
+ udid: 'phone-1',
170
+ host: nodeOrigin,
171
+ nodeId: exports.NODE_ID,
172
+ platform: 'android',
173
+ name: 'phone-1',
174
+ sdk: '14',
175
+ busy: false,
176
+ userBlocked: false,
177
+ offline: false,
178
+ },
179
+ });
180
+ });
181
+ }
182
+ function nodeSession() {
183
+ return __awaiter(this, arguments, void 0, function* (caps = {}) {
184
+ const token = yield typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer).createTokenFor({
185
+ userId: 'alice',
186
+ udid: 'phone-1',
187
+ host: nodeOrigin,
188
+ });
189
+ const res = yield (0, loopbackRequest_1.default)(nodeOrigin)
190
+ .post('/node/session')
191
+ .set(hubSessionToken_1.HUB_TOKEN_HEADER, token)
192
+ .send({
193
+ capabilities: {
194
+ alwaysMatch: Object.assign({ platformName: 'Android', 'appium:automationName': appium_umbrella_1.FAKE_AUTOMATION, 'appium:newCommandTimeout': 0, 'appium:udid': 'phone-1', 'xe:options': { recordVideo: false } }, caps),
195
+ firstMatch: [{}],
196
+ },
197
+ });
198
+ (0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
199
+ return res.body.value.sessionId;
200
+ });
201
+ }
202
+ const hubSide = (sessionId) => new RemoteSession_1.RemoteSession({
203
+ sessionId,
204
+ device: { udid: 'phone-1', host: nodeOrigin, nodeId: exports.NODE_ID, platform: 'android' },
205
+ sessionResponse: {},
206
+ xenonOption: {},
207
+ baseUrl: `${nodeOrigin}/node`,
208
+ });
209
+ function deleteOnNode(sessionId) {
210
+ return __awaiter(this, void 0, void 0, function* () {
211
+ const token = yield typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer).tokenFor(sessionId);
212
+ const res = yield (0, loopbackRequest_1.default)(nodeOrigin)
213
+ .delete(`/node/session/${sessionId}`)
214
+ .set(hubSessionToken_1.HUB_TOKEN_HEADER, token);
215
+ (0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
216
+ });
217
+ }
218
+ return {
219
+ get nodeOrigin() {
220
+ return nodeOrigin;
221
+ },
222
+ boot,
223
+ nodeSession,
224
+ hubSide,
225
+ deleteOnNode,
226
+ };
227
+ }