@xenon-device-management/xenon 2.16.0 → 2.17.1

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 (135) hide show
  1. package/README.md +2 -2
  2. package/lib/package.json +2 -3
  3. package/lib/public/assets/{AnnotationOverlay-2W4JvEyZ.js → AnnotationOverlay-CB8njQvS.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-GmckX_7r.js → ApiKeyGate-B-804QBS.js} +1 -1
  5. package/lib/public/assets/DeviceMosaicView-BsDvfjR1.js +2 -0
  6. package/lib/public/assets/{EmptyState-7hETKEWb.js → EmptyState-WzLw1cEV.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-GOgssFEP.js → FieldGroup-CaQNXgcE.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-Yvf6RGMd.js → FilterMenu-C9gAiypL.js} +1 -1
  9. package/lib/public/assets/{Menu-KrPnfdsf.js → Menu-CfTti3rw.js} +1 -1
  10. package/lib/public/assets/{Modal-C6WjZFeI.js → Modal-BcZUFhDm.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CSRIhZEh.js → RecordingPage-B_pbl3UE.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-CB8ST3nE.js → RecordingsPage-BkVhplE3.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-C9o90KId.js → SegmentedControl-CDLMLNl5.js} +1 -1
  14. package/lib/public/assets/{SettingCard-CPm3w9l2.js → SettingCard-Lyw5CB1F.js} +1 -1
  15. package/lib/public/assets/{Table-DwIgERRt.js → Table-CK414bOU.js} +1 -1
  16. package/lib/public/assets/{activity-DrLbHifn.js → activity-pEyAdE-M.js} +1 -1
  17. package/lib/public/assets/{ai-settings-DU_Ls06P.js → ai-settings-CIhS7MMq.js} +1 -1
  18. package/lib/public/assets/{api-keys-CWYdEzJ_.js → api-keys-D2J44Ebv.js} +1 -1
  19. package/lib/public/assets/{apps-BqNVdln7.js → apps-CCV3RZY8.js} +1 -1
  20. package/lib/public/assets/{arrow-left-BtB0_Jg_.js → arrow-left-DaEUmERk.js} +1 -1
  21. package/lib/public/assets/{arrow-right-C8NhdtyN.js → arrow-right-CnRXHXit.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-gMg0ffvv.js → arrow-up-right-BGCS7zhi.js} +1 -1
  23. package/lib/public/assets/{auth-shell-CiYZ3N4M.js → auth-shell-CpW1zsxD.js} +2 -2
  24. package/lib/public/assets/{builds-page-EaaqDmLN.js → builds-page-B-xdzjMx.js} +1 -1
  25. package/lib/public/assets/{button-DDau4Axv.js → button-B5QkOVa4.js} +1 -1
  26. package/lib/public/assets/{calendar-DQ83B1ed.js → calendar-gX_1EKue.js} +1 -1
  27. package/lib/public/assets/{check-K1ovTQam.js → check-MIM4THvH.js} +1 -1
  28. package/lib/public/assets/{chevron-right-Biq00pMt.js → chevron-right-BZIp3-9t.js} +1 -1
  29. package/lib/public/assets/{circle-check-CBv88cH7.js → circle-check-Crt-G_uy.js} +1 -1
  30. package/lib/public/assets/{circle-x-Bmltailn.js → circle-x-C8GvvXVt.js} +1 -1
  31. package/lib/public/assets/{clock-BPrld-3h.js → clock-MFKV4qGa.js} +1 -1
  32. package/lib/public/assets/{copy-t0QB14JI.js → copy-BU-Z7mBK.js} +1 -1
  33. package/lib/public/assets/device-explorer-D-78Twa0.js +282 -0
  34. package/lib/public/assets/{download-BroHxNOh.js → download-DIoLCCYv.js} +1 -1
  35. package/lib/public/assets/{forgot-password-Bjmf49i6.js → forgot-password-vNYlPnia.js} +1 -1
  36. package/lib/public/assets/{h264Stream-DeD__6wa.js → h264Stream-BLcSplMU.js} +1 -1
  37. package/lib/public/assets/{index-B0gQkEg0.js → index-BpqyclNY.js} +2 -2
  38. package/lib/public/assets/{index-B0JeuZbg.js → index-Hl1OL-k_.js} +1 -1
  39. package/lib/public/assets/{index-72oF89LJ.js → index-QbukVVrl.js} +1 -1
  40. package/lib/public/assets/{input-DIc4MSC7.js → input-DHH3fHer.js} +1 -1
  41. package/lib/public/assets/{line-chart-BcWm2vW8.js → line-chart-PdViEphw.js} +1 -1
  42. package/lib/public/assets/{list-checks-B06IJJad.js → list-checks-DI_heIg9.js} +1 -1
  43. package/lib/public/assets/{lock-pVLvnN30.js → lock-DVP_dL6J.js} +1 -1
  44. package/lib/public/assets/{login-CN77vy--.js → login-B8vtTDcr.js} +1 -1
  45. package/lib/public/assets/{maintenance-settings-CSdzQSkY.js → maintenance-settings-BMa5EfgI.js} +1 -1
  46. package/lib/public/assets/{monitor-BtrN5IkI.js → monitor-CtzX_RkD.js} +1 -1
  47. package/lib/public/assets/{mouse-pointer-2-CNsQfo6w.js → mouse-pointer-2-BA09sdS9.js} +1 -1
  48. package/lib/public/assets/{network-BSfA8ZxG.js → network-B_F9Nox-.js} +1 -1
  49. package/lib/public/assets/{overview-BfTebCK5.js → overview-kWCYMHMV.js} +1 -1
  50. package/lib/public/assets/{page-header-D8KUX6Qg.js → page-header-CAbH1O1a.js} +1 -1
  51. package/lib/public/assets/{play-Cvc8xzic.js → play-sg95hLqA.js} +1 -1
  52. package/lib/public/assets/{plus-C9Zj84UF.js → plus-BT9mk2gj.js} +1 -1
  53. package/lib/public/assets/{profile-page-Bi27STgs.js → profile-page-DZOz5Ypc.js} +1 -1
  54. package/lib/public/assets/{reset-password-CPsOuwSu.js → reset-password-CqeImlEO.js} +1 -1
  55. package/lib/public/assets/{runbook-page-DEQ-DBOX.js → runbook-page-U_8TSysj.js} +1 -1
  56. package/lib/public/assets/{select-BWpnhWXj.js → select-Cx4JDzsR.js} +1 -1
  57. package/lib/public/assets/{selector-detail-redirect-BefSCSpe.js → selector-detail-redirect-DiMRe7DW.js} +1 -1
  58. package/lib/public/assets/{selector-health-page-MSk96Zz7.js → selector-health-page-CKR5URll.js} +1 -1
  59. package/lib/public/assets/{session-detail-page-DPyBNef_.js → session-detail-page-DuIVzTW_.js} +1 -1
  60. package/lib/public/assets/{settings-V6x-cXwy.js → settings-B5fuvgTP.js} +1 -1
  61. package/lib/public/assets/{stat-tile-Duex-GZD.js → stat-tile-CtHX1dkh.js} +1 -1
  62. package/lib/public/assets/{tablet-BBcoqBmL.js → tablet-DCGD0GQt.js} +1 -1
  63. package/lib/public/assets/{teams-ts8p5rEh.js → teams-MzlELlw9.js} +1 -1
  64. package/lib/public/assets/{trash-2-CZ2ew6AZ.js → trash-2-BIRm2KVs.js} +1 -1
  65. package/lib/public/assets/{upload-Dv99y_le.js → upload-C-lgyswH.js} +1 -1
  66. package/lib/public/assets/{use-builds-data-DrYC8JCF.js → use-builds-data-IOydXA8d.js} +1 -1
  67. package/lib/public/assets/{use-password-reset-mode-C7QkBNkP.js → use-password-reset-mode-CIVXG7Me.js} +1 -1
  68. package/lib/public/assets/{users-gJ5Y01yo.js → users-hwK5XGIy.js} +1 -1
  69. package/lib/public/assets/{users-D7tKMESH.js → users-ivNCjeus.js} +1 -1
  70. package/lib/public/assets/{video-off-CqZVuZGs.js → video-off-DvcIEsdZ.js} +1 -1
  71. package/lib/public/assets/{webhook-settings-DI5DSW99.js → webhook-settings-DukgCe1u.js} +1 -1
  72. package/lib/public/assets/{zap-ClcHFxfo.js → zap-BE9gacFj.js} +1 -1
  73. package/lib/public/index.html +1 -1
  74. package/lib/schema.json +1 -1
  75. package/lib/src/app/index.js +2 -0
  76. package/lib/src/app/openapi/control.yaml +69 -4
  77. package/lib/src/app/openapi/grid.yaml +102 -0
  78. package/lib/src/app/routers/control.js +24 -5
  79. package/lib/src/dashboard/event-manager.js +5 -8
  80. package/lib/src/device-managers/ios/IOSStreamService.js +147 -18
  81. package/lib/src/device-utils.js +61 -24
  82. package/lib/src/gateway/internalCall.js +33 -5
  83. package/lib/src/gateway/nodeAsk.js +57 -0
  84. package/lib/src/gateway/nodeSessionDeviceLogs.js +35 -0
  85. package/lib/src/gateway/sessionGateway.js +9 -4
  86. package/lib/src/helpers/index.js +9 -6
  87. package/lib/src/interceptors/CommandInterceptor.js +10 -0
  88. package/lib/src/plugin.js +4 -1
  89. package/lib/src/scripts/initialize-database.js +0 -6
  90. package/lib/src/scripts/run-migrations.js +123 -880
  91. package/lib/src/services/ServerManager.js +25 -4
  92. package/lib/src/services/SessionLifecycleService.js +20 -5
  93. package/lib/src/services/UserService.js +8 -3
  94. package/lib/src/services/interceptor/CertManager.js +50 -16
  95. package/lib/src/services/logcat/NodeDeviceLogStore.js +99 -0
  96. package/lib/src/services/logcat/NodeDeviceLogsCollector.js +164 -0
  97. package/lib/src/services/logcat/SessionDeviceLogs.js +199 -44
  98. package/lib/src/services/logcat/nodeDeviceLogs.js +91 -0
  99. package/lib/src/services/metrics/nodeMetrics.js +20 -49
  100. package/lib/src/sessions/RemoteSession.js +11 -2
  101. package/lib/src/sessions/SessionManager.js +28 -1
  102. package/lib/src/sessions/appiumUmbrella.js +11 -0
  103. package/lib/src/sessions/sessionMemory.js +11 -2
  104. package/lib/test/helpers/fake-logcat.js +80 -0
  105. package/lib/test/helpers/hub-node-pair.js +227 -0
  106. package/lib/test/unit/UserService.test.js +8 -0
  107. package/lib/test/unit/boot-device-sync.spec.js +195 -0
  108. package/lib/test/unit/dashboard-events-device-logs.spec.js +15 -0
  109. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +21 -0
  110. package/lib/test/unit/hub-node-iphone-readiness.spec.js +134 -0
  111. package/lib/test/unit/hub-node-session-device-logs.spec.js +194 -0
  112. package/lib/test/unit/hub-node-session-metrics.spec.js +24 -192
  113. package/lib/test/unit/hub-remote-session-heartbeat.spec.js +8 -1
  114. package/lib/test/unit/install-scripts.spec.js +98 -0
  115. package/lib/test/unit/interceptor/CertManager.spec.js +89 -0
  116. package/lib/test/unit/internal-call.spec.js +99 -0
  117. package/lib/test/unit/internal-calls-session-record.spec.js +312 -0
  118. package/lib/test/unit/ios-simulator-preview.spec.js +315 -0
  119. package/lib/test/unit/is-xenon-running.spec.js +10 -0
  120. package/lib/test/unit/node-device-log-store.spec.js +102 -0
  121. package/lib/test/unit/node-device-logs-collector.spec.js +343 -0
  122. package/lib/test/unit/node-gateway-create-appium-server.spec.js +8 -1
  123. package/lib/test/unit/node-hub-session-owner.spec.js +8 -1
  124. package/lib/test/unit/node-session-device-logs-route.spec.js +103 -0
  125. package/lib/test/unit/run-migrations-database.spec.js +57 -424
  126. package/lib/test/unit/run-migrations.test.js +74 -674
  127. package/lib/test/unit/session-device-logs-hub-node.spec.js +340 -0
  128. package/lib/test/unit/session-device-logs-lifecycle.spec.js +137 -0
  129. package/lib/test/unit/session-device-logs.spec.js +32 -94
  130. package/lib/test/unit/session-log-indexes.spec.js +15 -0
  131. package/lib/tsconfig.tsbuildinfo +1 -1
  132. package/package.json +2 -3
  133. package/schema.json +1 -1
  134. package/lib/public/assets/DeviceMosaicView-DqODzkyo.js +0 -2
  135. package/lib/public/assets/device-explorer-BgGZ8AvV.js +0 -282
@@ -60,9 +60,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
60
60
  return (mod && mod.__esModule) ? mod : { "default": mod };
61
61
  };
62
62
  Object.defineProperty(exports, "__esModule", { value: true });
63
- exports.StreamRestartRefused = exports.IOS_IDLE_STOP_MS = void 0;
63
+ exports.SimulatorPreviewNeedsTest = exports.SIMULATOR_PREVIEW_NEEDS_TEST = exports.StreamRestartRefused = exports.IOS_IDLE_STOP_MS = void 0;
64
64
  exports.shouldStopIdleIosStream = shouldStopIdleIosStream;
65
65
  exports.heldByAppiumSession = heldByAppiumSession;
66
+ exports.isIosSimulator = isIosSimulator;
67
+ exports.simulatorPreviewRefusal = simulatorPreviewRefusal;
66
68
  const typedi_1 = require("typedi");
67
69
  const ProcessRegistry_1 = require("../../services/ProcessRegistry");
68
70
  const child_process_1 = require("child_process");
@@ -82,6 +84,7 @@ const iosStreamDiagnostics_1 = require("./iosStreamDiagnostics");
82
84
  const tunnelProcess_1 = require("./tunnelProcess");
83
85
  const IOSTunnels_1 = require("./IOSTunnels");
84
86
  const goIosBinary_1 = require("./goIosBinary");
87
+ const appiumUmbrella_1 = require("../../sessions/appiumUmbrella");
85
88
  const device_service_1 = require("../../data-service/device-service");
86
89
  const manualLock_1 = require("../../services/recording/manualLock");
87
90
  const recording_store_1 = require("../../services/recording/recording-store");
@@ -113,9 +116,39 @@ function heldByAppiumSession(device) {
113
116
  class StreamRestartRefused extends Error {
114
117
  }
115
118
  exports.StreamRestartRefused = StreamRestartRefused;
119
+ /** Why a simulator shows no live preview: for testers, on the device page and its tile. */
120
+ exports.SIMULATOR_PREVIEW_NEEDS_TEST = "A simulator's screen shows here only while a test runs on it.";
121
+ /** A simulator's preview was asked for while no test runs on it. */
122
+ class SimulatorPreviewNeedsTest extends Error {
123
+ constructor() {
124
+ super(exports.SIMULATOR_PREVIEW_NEEDS_TEST);
125
+ }
126
+ }
127
+ exports.SimulatorPreviewNeedsTest = SimulatorPreviewNeedsTest;
128
+ /**
129
+ * An iOS or tvOS simulator's row. `realDevice` is false only there among
130
+ * Apple rows (discovery writes it, the store reads a missing one as true); an
131
+ * Android emulator's is false too.
132
+ */
133
+ function isIosSimulator(device) {
134
+ return (device === null || device === void 0 ? void 0 : device.realDevice) === false && (device.platform === 'ios' || device.platform === 'tvos');
135
+ }
136
+ /**
137
+ * Why this phone can't be previewed now, or undefined when it can: a
138
+ * simulator no Appium session holds.
139
+ */
140
+ function simulatorPreviewRefusal(device) {
141
+ if (!isIosSimulator(device))
142
+ return undefined;
143
+ return heldByAppiumSession(device) ? undefined : exports.SIMULATOR_PREVIEW_NEEDS_TEST;
144
+ }
145
+ /** How many ended sessions a simulator preview remembers, to refuse a start mid-teardown. */
146
+ const ENDED_SESSIONS_KEPT = 200;
116
147
  let IOSStreamService = class IOSStreamService {
117
148
  constructor() {
118
149
  this.sessions = new Map();
150
+ /** Sessions whose simulator previews have ended (endSimulatorPreviews), newest last. */
151
+ this.endedSimulatorSessions = new Set();
119
152
  this.startFlight = new singleFlight_1.SingleFlight();
120
153
  this.recoveryCooldowns = new Map(); // Track last recovery attempt time
121
154
  this.RECOVERY_COOLDOWN_MS = 30000; // 30s cooldown between recovery attempts
@@ -411,6 +444,10 @@ let IOSStreamService = class IOSStreamService {
411
444
  const session = this.sessions.get(udid);
412
445
  if (!session || session.status !== 'running')
413
446
  return false;
447
+ // A simulator's preview has no process of its own to check or heal: the
448
+ // picture is its session's, and the preview ends with it.
449
+ if (session.simulatorSession)
450
+ return true;
414
451
  // 1. Process Check: verify child processes haven't exited
415
452
  const isWdaAlive = session.wdaProcess && session.wdaProcess.exitCode === null;
416
453
  const isWdaIproxyAlive = session.forwardWDAProcess && session.forwardWDAProcess.exitCode === null;
@@ -453,6 +490,9 @@ let IOSStreamService = class IOSStreamService {
453
490
  }
454
491
  startStream(udid) {
455
492
  return __awaiter(this, void 0, void 0, function* () {
493
+ const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
494
+ if (isIosSimulator(device))
495
+ return this.startSimulatorStream(udid, device);
456
496
  // Check if stream is already running - avoid unnecessary restarts
457
497
  const existingSession = this.sessions.get(udid);
458
498
  if (existingSession && existingSession.status === 'running') {
@@ -811,6 +851,82 @@ let IOSStreamService = class IOSStreamService {
811
851
  return this.startFlight.run(udid, performStartup);
812
852
  });
813
853
  }
854
+ /**
855
+ * A simulator's preview is its running test's picture. The XCUITest
856
+ * driver's WDA serves it on this Mac at the session's mjpegServerPort, so
857
+ * this starts nothing (go-ios, iproxy and runwda reach only an iPhone) and
858
+ * leases no port (the session's are its own). With no test running there
859
+ * is nothing to show: refused in plain words, with no cool-down, since a
860
+ * test may start any moment.
861
+ *
862
+ * Through 2.15 a simulator went through the iPhone's start. With no test it
863
+ * failed, and left its iproxy forwards on the ports it had leased to the
864
+ * simulator: a test started meanwhile was given those ports and its WDA
865
+ * couldn't open them. With a test it read the picture through a forward
866
+ * that carries nothing, so a Live devices tile stayed empty.
867
+ */
868
+ startSimulatorStream(udid, device) {
869
+ return __awaiter(this, void 0, void 0, function* () {
870
+ var _a;
871
+ let sessionId = simulatorPreviewRefusal(device) ? undefined : ((_a = device.session_id) !== null && _a !== void 0 ? _a : undefined);
872
+ // An ended test's row can still name it while its phone is being released.
873
+ if (sessionId && this.endedSimulatorSessions.has(sessionId))
874
+ sessionId = undefined;
875
+ // Only the ports of a session Appium has now: the row's would outlive it.
876
+ const driver = sessionId ? typedi_2.Container.get(appiumUmbrella_1.AppiumUmbrella).driverOptions(sessionId) : undefined;
877
+ const wdaPort = Number(driver === null || driver === void 0 ? void 0 : driver.wdaLocalPort);
878
+ const mjpegPort = Number(driver === null || driver === void 0 ? void 0 : driver.mjpegServerPort);
879
+ const existing = this.sessions.get(udid);
880
+ if (!sessionId || !wdaPort || !mjpegPort) {
881
+ if (existing)
882
+ yield this.stopStream(udid);
883
+ throw new SimulatorPreviewNeedsTest();
884
+ }
885
+ if ((existing === null || existing === void 0 ? void 0 : existing.simulatorSession) === sessionId && existing.status === 'running') {
886
+ return { wdaPort: existing.wdaPort, mjpegPort: existing.mjpegPort };
887
+ }
888
+ // An earlier test's preview, or anything else held for this simulator.
889
+ if (existing)
890
+ yield this.stopStream(udid);
891
+ this.sessions.set(udid, {
892
+ udid,
893
+ wdaProcess: null,
894
+ forwardWDAProcess: null,
895
+ forwardMJPEGProcess: null,
896
+ tunnelPort: null,
897
+ wdaPort,
898
+ mjpegPort,
899
+ status: 'running',
900
+ startedAt: new Date(),
901
+ lastViewerAt: Date.now(),
902
+ viewerCount: 0,
903
+ simulatorSession: sessionId,
904
+ });
905
+ logger_1.default.info(`[${udid}] Simulator preview: the picture of session ${sessionId} (port ${mjpegPort})`);
906
+ return { wdaPort, mjpegPort };
907
+ });
908
+ }
909
+ /**
910
+ * End the simulator previews that show this session's picture. Called
911
+ * however the session ends (forgetSessionMemory): the driver's WDA may
912
+ * outlive it, but no test runs any more.
913
+ */
914
+ endSimulatorPreviews(sessionId) {
915
+ return __awaiter(this, void 0, void 0, function* () {
916
+ this.endedSimulatorSessions.add(sessionId);
917
+ if (this.endedSimulatorSessions.size > ENDED_SESSIONS_KEPT) {
918
+ const oldest = this.endedSimulatorSessions.values().next().value;
919
+ if (oldest !== undefined)
920
+ this.endedSimulatorSessions.delete(oldest);
921
+ }
922
+ for (const [udid, session] of this.sessions.entries()) {
923
+ if (session.simulatorSession !== sessionId)
924
+ continue;
925
+ logger_1.default.info(`[${udid}] Simulator preview ends with session ${sessionId}`);
926
+ yield this.stopStream(udid);
927
+ }
928
+ });
929
+ }
814
930
  killStaleProcesses(udid, wdaPort, mjpegPort) {
815
931
  return __awaiter(this, void 0, void 0, function* () {
816
932
  logger_1.default.info(`Cleaning up stale processes for ${udid} (Ports: ${wdaPort}, ${mjpegPort})`);
@@ -879,10 +995,16 @@ let IOSStreamService = class IOSStreamService {
879
995
  */
880
996
  stopStream(udid_1) {
881
997
  return __awaiter(this, arguments, void 0, function* (udid, opts = {}) {
882
- var _a;
883
998
  const session = this.sessions.get(udid);
884
999
  if (!session)
885
1000
  return;
1001
+ // A simulator's preview started nothing and leased no port: the ports and
1002
+ // WDA are its session's. Only the entry goes (and a manual hold, below).
1003
+ if (session.simulatorSession) {
1004
+ this.sessions.delete(udid);
1005
+ yield this.releaseManualHold(udid);
1006
+ return;
1007
+ }
886
1008
  if (opts.forViewer &&
887
1009
  session.wdaProcess &&
888
1010
  (yield this.appiumSessionMayUse(udid, "the stream's WDA and go-ios tunnel"))) {
@@ -902,22 +1024,7 @@ let IOSStreamService = class IOSStreamService {
902
1024
  // A stream attached to an Appium session's WDA started none.
903
1025
  if (session.tunnelPort != null)
904
1026
  yield this.tunnels().stop(udid);
905
- // Principal Fix: Only release the device lock if THIS STREAM SERVICE owns it.
906
- // The lock could belong to an Appium automation session (session_id is a real UUID).
907
- // We should only unblock if it's a manual control lock (session_id starts with 'manual_').
908
- try {
909
- const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
910
- if (device && ((_a = device.session_id) === null || _a === void 0 ? void 0 : _a.startsWith('manual_'))) {
911
- logger_1.default.info(`Stream Stop: Releasing manual control lock for ${udid}`);
912
- yield (0, device_service_1.unblockDevice)(udid, device.host);
913
- }
914
- else if (device && device.busy) {
915
- logger_1.default.info(`Stream Stop: Device ${udid} is busy with session ${device.session_id}. NOT releasing lock.`);
916
- }
917
- }
918
- catch (e) {
919
- logger_1.default.error(`Failed to check/release lock during stream stop for ${udid}: ${e}`);
920
- }
1027
+ yield this.releaseManualHold(udid);
921
1028
  // Also clean up any orphan processes (belt and suspenders)
922
1029
  yield this.cleanupOrphanTunnels(udid);
923
1030
  // Release the port leases so the allocator can reuse these ports for other
@@ -933,6 +1040,28 @@ let IOSStreamService = class IOSStreamService {
933
1040
  this.sessions.delete(udid);
934
1041
  });
935
1042
  }
1043
+ /**
1044
+ * Release the phone's manual hold (a preview's, a recording's), never an
1045
+ * Appium session's claim: that session_id is a real id, not `manual_...`.
1046
+ */
1047
+ releaseManualHold(udid) {
1048
+ return __awaiter(this, void 0, void 0, function* () {
1049
+ var _a;
1050
+ try {
1051
+ const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
1052
+ if (device && ((_a = device.session_id) === null || _a === void 0 ? void 0 : _a.startsWith('manual_'))) {
1053
+ logger_1.default.info(`Stream Stop: Releasing manual control lock for ${udid}`);
1054
+ yield (0, device_service_1.unblockDevice)(udid, device.host);
1055
+ }
1056
+ else if (device && device.busy) {
1057
+ logger_1.default.info(`Stream Stop: Device ${udid} is busy with session ${device.session_id}. NOT releasing lock.`);
1058
+ }
1059
+ }
1060
+ catch (e) {
1061
+ logger_1.default.error(`Failed to check/release lock during stream stop for ${udid}: ${e}`);
1062
+ }
1063
+ });
1064
+ }
936
1065
  updateWDASettings(wdaPort) {
937
1066
  return __awaiter(this, void 0, void 0, function* () {
938
1067
  const urls = [`http://127.0.0.1:${wdaPort}/appium/settings`];
@@ -45,7 +45,7 @@ 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.getDeviceTypeFromApp = void 0;
48
+ exports.BOOT_PROBE = exports.getDeviceTypeFromApp = void 0;
49
49
  exports.isAndroid = isAndroid;
50
50
  exports.deviceType = deviceType;
51
51
  exports.isIOS = isIOS;
@@ -59,6 +59,8 @@ exports.setUtilizationTime = setUtilizationTime;
59
59
  exports.getDeviceFiltersFromCapability = getDeviceFiltersFromCapability;
60
60
  exports.getRunningSessionsCount = getRunningSessionsCount;
61
61
  exports.updateDeviceList = updateDeviceList;
62
+ exports.syncLocalDevices = syncLocalDevices;
63
+ exports.reportDevicesToHub = reportDevicesToHub;
62
64
  exports.refreshSimulatorState = refreshSimulatorState;
63
65
  exports.setupCronCheckStaleDevices = setupCronCheckStaleDevices;
64
66
  exports.removeStaleDevices = removeStaleDevices;
@@ -275,8 +277,17 @@ function allocateDeviceForSession(capability_1, deviceTimeOutMs_1, deviceQueryIn
275
277
  // We have a locked device here
276
278
  if (device !== null) {
277
279
  const lockedDevice = device;
278
- // Principal Health Check Integration: Ensure device is READY before allocation
279
- if (!lockedDevice.cloud) {
280
+ // Ready for a session: asked only of a phone this server drives. The
281
+ // check is this machine's (an iPhone's WebDriverAgent on 127.0.0.1, and a
282
+ // go-ios stream started to recover it). Another server's phone, a node's
283
+ // on a hub, is checked by that server when it allocates the phone the hub
284
+ // pins. Through 2.15 the hub checked it too: with the node on another
285
+ // machine nothing answered and the session was refused as unhealthy, and
286
+ // with both on one Mac the hub started a WebDriverAgent of its own on the
287
+ // node's iPhone.
288
+ const context = typedi_1.Container.get(PluginContext_1.PluginContext);
289
+ const ownPhone = (0, localDeviceHosts_1.isOwnDevice)((0, localDeviceHosts_1.localDeviceHosts)(context.pluginArgs, context.port), context.nodeId, lockedDevice);
290
+ if (!lockedDevice.cloud && ownPhone) {
280
291
  const platform = lockedDevice.platform.toLowerCase();
281
292
  const managers = yield getDeviceManager().deviceInstances();
282
293
  const manager = managers.find((m) => {
@@ -521,6 +532,19 @@ function getRunningSessionsCount() {
521
532
  });
522
533
  }
523
534
  function updateDeviceList(local, hubArgument, tlsRejectUnauthorized) {
535
+ return __awaiter(this, void 0, void 0, function* () {
536
+ const { devices, gone } = yield syncLocalDevices(local);
537
+ if (hubArgument) {
538
+ yield reportDevicesToHub(hubArgument, devices, gone, tlsRejectUnauthorized);
539
+ }
540
+ return devices;
541
+ });
542
+ }
543
+ /**
544
+ * This server's own phones: what discovery reports now is written to the
545
+ * table, and its own rows discovery no longer reports are removed (`gone`).
546
+ */
547
+ function syncLocalDevices(local) {
524
548
  return __awaiter(this, void 0, void 0, function* () {
525
549
  const allExistingDevices = yield (0, device_service_1.getAllDevices)();
526
550
  const devices = yield getDeviceManager().getDevices(allExistingDevices);
@@ -538,28 +562,31 @@ function updateDeviceList(local, hubArgument, tlsRejectUnauthorized) {
538
562
  logger_1.default.info(`Removing ${staleLocalDevices.length} stale devices/simulators no longer discovered on this host.`);
539
563
  yield (0, device_service_1.removeDevice)(staleLocalDevices.map((d) => ({ udid: d.udid, host: d.host })));
540
564
  }
541
- if (hubArgument) {
542
- if (yield (0, helpers_1.isXenonRunning)(hubArgument, tlsRejectUnauthorized)) {
543
- const nodeDevices = new NodeDevices_1.default(hubArgument, {
544
- tlsRejectUnauthorized,
545
- hubAccessKey: config_1.config.hubAccessKey,
546
- hubToken: config_1.config.hubToken,
547
- });
548
- try {
549
- yield nodeDevices.postDevicesToHub(devices, 'add');
550
- if (staleLocalDevices.length > 0) {
551
- yield nodeDevices.postDevicesToHub(staleLocalDevices, 'remove');
552
- }
553
- }
554
- catch (error) {
555
- logger_1.default.error(`Cannot send device list update. Reason: ${error}`);
565
+ return { devices, gone: staleLocalDevices };
566
+ });
567
+ }
568
+ /** A node tells its hub the phones it has, and the ones it no longer has. */
569
+ function reportDevicesToHub(hubArgument, devices, gone, tlsRejectUnauthorized) {
570
+ return __awaiter(this, void 0, void 0, function* () {
571
+ if (yield (0, helpers_1.isXenonRunning)(hubArgument, tlsRejectUnauthorized)) {
572
+ const nodeDevices = new NodeDevices_1.default(hubArgument, {
573
+ tlsRejectUnauthorized,
574
+ hubAccessKey: config_1.config.hubAccessKey,
575
+ hubToken: config_1.config.hubToken,
576
+ });
577
+ try {
578
+ yield nodeDevices.postDevicesToHub(devices, 'add');
579
+ if (gone.length > 0) {
580
+ yield nodeDevices.postDevicesToHub(gone, 'remove');
556
581
  }
557
582
  }
558
- else {
559
- logger_1.default.warn(`Not sending device update since hub ${hubArgument} is not running`);
583
+ catch (error) {
584
+ logger_1.default.error(`Cannot send device list update. Reason: ${error}`);
560
585
  }
561
586
  }
562
- return devices;
587
+ else {
588
+ logger_1.default.warn(`Not sending device update since hub ${hubArgument} is not running`);
589
+ }
563
590
  });
564
591
  }
565
592
  function refreshSimulatorState(pluginArgs, hostPort) {
@@ -581,11 +608,21 @@ function setupCronCheckStaleDevices(intervalMs, local, tlsRejectUnauthorized) {
581
608
  }), intervalMs);
582
609
  });
583
610
  }
611
+ /**
612
+ * How a start asks another server whether it is alive: once, for at most 5 s.
613
+ * Appium opens its port only once every plugin's updateServer has finished,
614
+ * and the client's own way (four 30 s tries) held it closed for two minutes
615
+ * when a row named a host that drops packets. A live server answers in
616
+ * milliseconds; one that answers slower has its phones dropped and reports
617
+ * them again at its next report.
618
+ */
619
+ exports.BOOT_PROBE = { timeoutMs: 5000, retry: false };
584
620
  /**
585
621
  * Remove devices where the host is not alive nor defined.
586
622
  * @param local the hosts this server files its own phones under
623
+ * @param probe how each host is asked (the client's own way when not given)
587
624
  */
588
- function removeStaleDevices(local, tlsRejectUnauthorized) {
625
+ function removeStaleDevices(local, tlsRejectUnauthorized, probe) {
589
626
  return __awaiter(this, void 0, void 0, function* () {
590
627
  const allDevices = yield (0, device_service_1.getAllDevices)();
591
628
  const nodeId = typedi_1.Container.get(PluginContext_1.PluginContext).nodeId;
@@ -602,13 +639,13 @@ function removeStaleDevices(local, tlsRejectUnauthorized) {
602
639
  const aliveHosts = (yield Promise.allSettled(nodeHosts.map((host) => __awaiter(this, void 0, void 0, function* () {
603
640
  return {
604
641
  host: host,
605
- alive: yield (0, helpers_1.isXenonRunning)(host, tlsRejectUnauthorized),
642
+ alive: yield (0, helpers_1.isXenonRunning)(host, tlsRejectUnauthorized, probe),
606
643
  };
607
644
  }))));
608
645
  const aliveCloudHosts = (yield Promise.allSettled(cloudHosts.map((host) => __awaiter(this, void 0, void 0, function* () {
609
646
  return {
610
647
  host: host,
611
- alive: yield (0, helpers_1.isAppiumRunningAt)(host, tlsRejectUnauthorized),
648
+ alive: yield (0, helpers_1.isAppiumRunningAt)(host, tlsRejectUnauthorized, probe),
612
649
  };
613
650
  }))));
614
651
  // summarize alive hosts
@@ -4,8 +4,10 @@ exports.INTERNAL_CALL_MARKER = exports.INTERNAL_CALL_HEADER = void 0;
4
4
  exports.internalCallHeaders = internalCallHeaders;
5
5
  exports.hasInternalCallSecret = hasInternalCallSecret;
6
6
  exports.isInternalCall = isInternalCall;
7
+ exports.isInsideInternalCall = isInsideInternalCall;
7
8
  exports.internalCallBaseUrl = internalCallBaseUrl;
8
9
  exports.createInternalCallLayer = createInternalCallLayer;
10
+ const async_hooks_1 = require("async_hooks");
9
11
  const crypto_1 = require("crypto");
10
12
  const appiumBasePath_1 = require("../app/appiumBasePath");
11
13
  /**
@@ -27,11 +29,20 @@ const appiumBasePath_1 = require("../app/appiumBasePath");
27
29
  * The secret never leaves the process: it is sent only to this server's own
28
30
  * address, with any HTTP proxy bypassed, and the gateway removes the header
29
31
  * before anything after it sees the request.
32
+ *
33
+ * Such a call is never one of the test's commands. The rest of its request
34
+ * runs in an async context the plugin can read (`isInsideInternalCall`), since
35
+ * the plugin's `handle`, deep inside Appium's route, is never given the
36
+ * request. Through 2.16 it couldn't tell, so the dashboard recorded the call
37
+ * as the session's own: a performance recording's stop the driver refused at
38
+ * the end of an iPhone session (through 2.15, of every simulator session)
39
+ * failed a session whose commands had all passed.
30
40
  */
31
41
  exports.INTERNAL_CALL_HEADER = 'x-xenon-internal';
32
42
  exports.INTERNAL_CALL_MARKER = '/wd-internal';
33
43
  const SECRET = Buffer.from((0, crypto_1.randomBytes)(32).toString('base64url'));
34
44
  const INTERNAL = Symbol('xenon.internalCall');
45
+ const internalCalls = new async_hooks_1.AsyncLocalStorage();
35
46
  /** The headers a loopback call to this process's own server carries. */
36
47
  function internalCallHeaders() {
37
48
  return { [exports.INTERNAL_CALL_HEADER]: SECRET.toString() };
@@ -48,6 +59,19 @@ function hasInternalCallSecret(headers) {
48
59
  function isInternalCall(req) {
49
60
  return !!req && req[INTERNAL] === true;
50
61
  }
62
+ /**
63
+ * Whether this code runs for a request the internal-call layer accepted: the
64
+ * plugin's `handle` and the driver's command under it, which never see the
65
+ * request itself.
66
+ *
67
+ * Read it where a command enters the plugin (CommandInterceptor.handle). It
68
+ * also holds in whatever the driver starts during the call and runs later (a
69
+ * timer, a socket's callback): base-driver's new-command timer, restarted at
70
+ * the call's end, fires inside it, and so does an idle session's shutdown.
71
+ */
72
+ function isInsideInternalCall() {
73
+ return internalCalls.getStore() === true;
74
+ }
51
75
  /**
52
76
  * Where a local session's loopback calls go: this server, under its base
53
77
  * path, marked `/wd-internal`. A wildcard bind address is reached on 127.0.0.1.
@@ -80,11 +104,15 @@ function createInternalCallLayer(basePath) {
80
104
  req.originalUrl = req.url;
81
105
  return true;
82
106
  }
83
- // One call to next() for every outcome: Appium's 404 carries a stack trace,
84
- // and a second call site would make a refused /wd-internal request's answer
85
- // differ from any other unknown route's by that one frame.
107
+ // Every request this layer leaves alone goes on through the one call to
108
+ // next() below: Appium's 404 carries a stack trace, and a second call site
109
+ // would make a refused /wd-internal request's answer differ from any other
110
+ // unknown route's by that one frame. An accepted call goes on inside its
111
+ // context, down to the plugin's handle (isInsideInternalCall).
86
112
  return function xenonInternalCalls(req, _res, next) {
87
- accept(req);
88
- next();
113
+ if (accept(req))
114
+ internalCalls.run(true, next);
115
+ else
116
+ next();
89
117
  };
90
118
  }
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OlderNodes = exports.OLDER_NODE_RECHECK_MS = void 0;
4
+ exports.readNodeReply = readNodeReply;
5
+ /**
6
+ * The node's answer at a route whose answers carry `header`. `answerOf` reads
7
+ * a 200's `value`, or returns null when it isn't one.
8
+ */
9
+ function readNodeReply(header, status, headers, data, answerOf) {
10
+ if (!(headers === null || headers === void 0 ? void 0 : headers[header])) {
11
+ // An older node answers its login's 401, an unknown route's 404, or a
12
+ // catch-all's 2xx. Anything else without the header (a proxy's 502 in
13
+ // front of a node) is an outage: ask again, never give the node up.
14
+ if (status === 401 || status === 404 || (status >= 200 && status < 300)) {
15
+ return { kind: 'unsupported', status };
16
+ }
17
+ return { kind: 'unavailable', reason: `answered ${status}` };
18
+ }
19
+ if (status === 404)
20
+ return { kind: 'refused' };
21
+ const answer = status === 200 ? answerOf(data === null || data === void 0 ? void 0 : data.value) : null;
22
+ if (answer !== null)
23
+ return { kind: 'answer', answer };
24
+ return { kind: 'unavailable', reason: `answered ${status}` };
25
+ }
26
+ /** How long a hub leaves a node without a route alone before asking it again. */
27
+ exports.OLDER_NODE_RECHECK_MS = 10 * 60000;
28
+ /**
29
+ * Hub side: which nodes lack a route (an older Xenon). Their sessions go
30
+ * without what the route serves; the hub says so once per node, and asks
31
+ * again after OLDER_NODE_RECHECK_MS, so a node upgraded in place is picked
32
+ * up without restarting the hub.
33
+ */
34
+ class OlderNodes {
35
+ constructor() {
36
+ this.now = () => Date.now();
37
+ this.unsupportedUntil = new Map();
38
+ this.warned = new Set();
39
+ }
40
+ shouldAsk(origin) {
41
+ const until = this.unsupportedUntil.get(origin);
42
+ if (until === undefined)
43
+ return true;
44
+ if (until > this.now())
45
+ return false;
46
+ this.unsupportedUntil.delete(origin);
47
+ return true;
48
+ }
49
+ unsupported(origin, status) {
50
+ this.unsupportedUntil.set(origin, this.now() + exports.OLDER_NODE_RECHECK_MS);
51
+ if (this.warned.has(origin))
52
+ return;
53
+ this.warned.add(origin);
54
+ this.logger.warn(this.warning(origin, status));
55
+ }
56
+ }
57
+ exports.OlderNodes = OlderNodes;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.nodeSessionDeviceLogsHandler = nodeSessionDeviceLogsHandler;
7
+ exports.registerNodeSessionDeviceLogs = registerNodeSessionDeviceLogs;
8
+ const typedi_1 = require("typedi");
9
+ const logger_1 = __importDefault(require("../logger"));
10
+ const config_1 = require("../config");
11
+ const commandAuth_1 = require("../middleware/commandAuth");
12
+ const nodeDeviceLogs_1 = require("../services/logcat/nodeDeviceLogs");
13
+ const NodeDeviceLogStore_1 = require("../services/logcat/NodeDeviceLogStore");
14
+ const hubSessionToken_1 = require("./hubSessionToken");
15
+ const nodeSessionMetrics_1 = require("./nodeSessionMetrics");
16
+ const nodeSessionStatus_1 = require("./nodeSessionStatus");
17
+ function nodeSessionDeviceLogsHandler(deps) {
18
+ return (req, res) => {
19
+ var _a, _b;
20
+ res.setHeader(nodeDeviceLogs_1.NODE_DEVICE_LOGS_HEADER, '1');
21
+ const sessionId = String((_b = (_a = req.params) === null || _a === void 0 ? void 0 : _a.sessionId) !== null && _b !== void 0 ? _b : '');
22
+ (0, nodeSessionStatus_1.answerForHub)(deps, req, res, sessionId, 'Session device logs', () => { var _a; return res.status(200).json({ value: deps.read(sessionId, (0, nodeSessionMetrics_1.parseAfter)((_a = req.query) === null || _a === void 0 ? void 0 : _a.after)) }); });
23
+ };
24
+ }
25
+ /** A node's route, ahead of its login, like the session-status route. A hub or standalone server has none. */
26
+ function registerNodeSessionDeviceLogs(router, pluginArgs) {
27
+ if (pluginArgs.hub === undefined)
28
+ return;
29
+ router.get(nodeDeviceLogs_1.NODE_DEVICE_LOGS_ROUTE, nodeSessionDeviceLogsHandler({
30
+ hubTokens: new hubSessionToken_1.HubSessionTokenVerifier(pluginArgs.hub),
31
+ enforced: () => (0, commandAuth_1.commandAuthEnabled)() && config_1.config.authDisabled !== true,
32
+ read: (sessionId, after) => typedi_1.Container.get(NodeDeviceLogStore_1.NodeDeviceLogStore).read(sessionId, after),
33
+ logger: logger_1.default.scope('NodeSessionDeviceLogs'),
34
+ }));
35
+ }
@@ -168,10 +168,15 @@ function createHubRouting(deps) {
168
168
  yield deleteRemote(req, res, sessionId, location);
169
169
  return;
170
170
  }
171
- yield deps
172
- .touch(sessionId)
173
- .catch((error) => logger.warn(`Could not record activity for session ${sessionId}: ${(0, commandAuth_1.summarize)(error)}`));
174
- const hooks = location.session && !location.cloud ? deps.dashboard : undefined;
171
+ // Xenon's own call (internalCall.ts) is never one of the test's
172
+ // commands: it isn't the session's activity, nor in its record.
173
+ const own = (0, internalCall_1.isInternalCall)(req);
174
+ if (!own) {
175
+ yield deps
176
+ .touch(sessionId)
177
+ .catch((error) => logger.warn(`Could not record activity for session ${sessionId}: ${(0, commandAuth_1.summarize)(error)}`));
178
+ }
179
+ const hooks = location.session && !location.cloud && !own ? deps.dashboard : undefined;
175
180
  const command = hooks
176
181
  ? deps.commandName(`${deps.basePath}/session/${sessionId}${rest.split('?')[0]}`, req.method)
177
182
  : undefined;
@@ -176,15 +176,18 @@ function stripAppiumPrefixes(caps) {
176
176
  }
177
177
  return strippedCaps;
178
178
  }
179
- function isXenonRunning(host, tlsRejectUnauthorized) {
180
- return __awaiter(this, void 0, void 0, function* () {
179
+ function probeConfig(probe) {
180
+ return Object.assign(Object.assign({}, (probe.timeoutMs !== undefined ? { timeout: probe.timeoutMs } : {})), (probe.retry === false ? { retry: false } : {}));
181
+ }
182
+ function isXenonRunning(host_1, tlsRejectUnauthorized_1) {
183
+ return __awaiter(this, arguments, void 0, function* (host, tlsRejectUnauthorized, probe = {}) {
181
184
  try {
182
185
  const client = InternalHttpClient_1.InternalHttpClient.getClient(tlsRejectUnauthorized);
183
186
  // /health is the one /xenon/api route with no login. The hub probes its
184
187
  // nodes, and a node its hub, without credentials, so a login-only route
185
188
  // (/status) called a server with auth on dead: the hub pruned its node's
186
189
  // phones, and the node never registered with its hub.
187
- yield client.get(`${host}/xenon/api/health`);
190
+ yield client.get(`${host}/xenon/api/health`, probeConfig(probe));
188
191
  return true;
189
192
  }
190
193
  catch (error) {
@@ -193,11 +196,11 @@ function isXenonRunning(host, tlsRejectUnauthorized) {
193
196
  }
194
197
  });
195
198
  }
196
- function isAppiumRunningAt(url, tlsRejectUnauthorized) {
197
- return __awaiter(this, void 0, void 0, function* () {
199
+ function isAppiumRunningAt(url_1, tlsRejectUnauthorized_1) {
200
+ return __awaiter(this, arguments, void 0, function* (url, tlsRejectUnauthorized, probe = {}) {
198
201
  try {
199
202
  const client = InternalHttpClient_1.InternalHttpClient.getClient(tlsRejectUnauthorized);
200
- yield client.get(`${url}/status`);
203
+ yield client.get(`${url}/status`, probeConfig(probe));
201
204
  return true;
202
205
  }
203
206
  catch (error) {
@@ -70,6 +70,7 @@ const waitFor_1 = require("../services/autowait/waitFor");
70
70
  const SelfHealingSwitch_1 = require("../services/settings/SelfHealingSwitch");
71
71
  const xenonScripts_1 = require("./xenonScripts");
72
72
  const healReport_1 = require("../gateway/healReport");
73
+ const internalCall_1 = require("../gateway/internalCall");
73
74
  const W3C_ELEMENT_KEY = 'element-6066-11e4-a52e-4f735466cecf';
74
75
  /**
75
76
  * The commands the interceptor answers itself for an element Xenon found in a
@@ -116,6 +117,15 @@ let CommandInterceptor = class CommandInterceptor {
116
117
  const IGNORED_COMMANDS = ['getScreenshot', 'stopRecordingScreen', 'startRecordingScreen'];
117
118
  if (IGNORED_COMMANDS.includes(commandName))
118
119
  return yield next();
120
+ // Xenon's own call to the session (a LocalSession's loopback, when its
121
+ // in-process call failed; gateway/internalCall.ts) goes straight to the
122
+ // driver. It isn't one of the test's commands: it isn't recorded with
123
+ // them, so it can't decide the session's status, and it doesn't reset
124
+ // Xenon's idle clock. Through 2.16 a performance recording's stop the
125
+ // driver refused at the end was recorded as a failed `execute`, and the
126
+ // session ended failed.
127
+ if ((0, internalCall_1.isInsideInternalCall)())
128
+ return yield next();
119
129
  const sessionId = driver.sessionId || args[args.length - 1];
120
130
  const tracingService = typedi_1.Container.get(TracingService_1.TracingService);
121
131
  const spanId = `${sessionId}:${commandName}`;
package/lib/src/plugin.js CHANGED
@@ -68,6 +68,7 @@ const appiumUmbrella_1 = require("./sessions/appiumUmbrella");
68
68
  const LiveSessionOwners_1 = require("./services/device-access/LiveSessionOwners");
69
69
  const sessionMemory_1 = require("./sessions/sessionMemory");
70
70
  const SessionMetricsService_1 = require("./services/metrics/SessionMetricsService");
71
+ const SessionDeviceLogs_1 = require("./services/logcat/SessionDeviceLogs");
71
72
  const PhoneNetworkRestore_1 = require("./services/network/PhoneNetworkRestore");
72
73
  const config_1 = require("./config");
73
74
  const SessionManager_1 = require("./sessions/SessionManager");
@@ -120,8 +121,10 @@ class XenonPlugin extends base_plugin_1.default {
120
121
  reason: (0, shutdownReason_1.unexpectedShutdownReason)(cause),
121
122
  });
122
123
  }
123
- if (sessionId)
124
+ if (sessionId) {
124
125
  yield typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService).stop(sessionId);
126
+ yield typedi_1.Container.get(SessionDeviceLogs_1.SessionDeviceLogs).stop(sessionId);
127
+ }
125
128
  // Appium's new-command timeout ends a session here, not in deleteSession:
126
129
  // the phone's network (profile, interceptor proxy) and the capture are
127
130
  // put back before the phone is released.