@xenon-device-management/xenon 2.9.1 → 2.10.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 (148) hide show
  1. package/lib/package.json +1 -1
  2. package/lib/public/assets/{AnnotationOverlay-Dm_URoOL.js → AnnotationOverlay-LzfXiiZJ.js} +1 -1
  3. package/lib/public/assets/{ApiKeyGate-BdGOuXDm.js → ApiKeyGate-BzFDp4mK.js} +1 -1
  4. package/lib/public/assets/{BugReportButton-BuWGEjgw.js → BugReportButton-BpgWvmZo.js} +1 -1
  5. package/lib/public/assets/{DeviceMosaicView-Pu3rJ99D.js → DeviceMosaicView-CCB-egSH.js} +2 -2
  6. package/lib/public/assets/{EmptyState-syyHElMd.js → EmptyState-DkUVmANo.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-BZR5ZBVv.js → FieldGroup-mbfnfTql.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-CsgtrmPb.js → FilterMenu-BH5usz4Y.js} +1 -1
  9. package/lib/public/assets/{Menu-DP8XOm1A.js → Menu-Cbo9mdTk.js} +1 -1
  10. package/lib/public/assets/{Modal-Dtd-8uI2.js → Modal-D6XfQHPr.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CdAtAmHj.js → RecordingPage-CmFPkWqH.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-Br3CKxTe.js → RecordingsPage-CSlKk2qC.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-BOaNmUL-.js → SegmentedControl-BFW9Dlqd.js} +1 -1
  14. package/lib/public/assets/{SettingCard-CQuK9ZZX.js → SettingCard-C4DioTTi.js} +1 -1
  15. package/lib/public/assets/{Table-CswCefnT.js → Table-DrKjlL00.js} +1 -1
  16. package/lib/public/assets/{activity-6fhH3u4C.js → activity-C9bE7Jm8.js} +1 -1
  17. package/lib/public/assets/{ai-settings-CU24L3D4.js → ai-settings-DRYic4oO.js} +1 -1
  18. package/lib/public/assets/{api-keys-DC3BZDaG.js → api-keys-CaQU0g7c.js} +1 -1
  19. package/lib/public/assets/{apps-Ce5Z-wNc.js → apps-c1gUFmmK.js} +1 -1
  20. package/lib/public/assets/{arrow-left-DZJqYn7r.js → arrow-left-DMvAWA8r.js} +1 -1
  21. package/lib/public/assets/{arrow-right-nIvK8CyN.js → arrow-right-BEQgxmKT.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-BfugJj3f.js → arrow-up-right-Ba0e2GeU.js} +1 -1
  23. package/lib/public/assets/{auth-shell-B1Cz75Vq.js → auth-shell-COODQnc7.js} +2 -2
  24. package/lib/public/assets/{builds-page-5mlJGjIk.js → builds-page-DZQgWBUs.js} +4 -4
  25. package/lib/public/assets/{button-D_W88sn-.js → button-WW9j43wR.js} +1 -1
  26. package/lib/public/assets/{calendar-Dc6pWZS9.js → calendar-COUs9VxF.js} +1 -1
  27. package/lib/public/assets/{check-BbvtnP8m.js → check-D8ifHIJR.js} +1 -1
  28. package/lib/public/assets/{chevron-right-CgBUaRCA.js → chevron-right-BjKVUlB_.js} +1 -1
  29. package/lib/public/assets/{circle-check-BWjEKvA4.js → circle-check-CfP79QE2.js} +1 -1
  30. package/lib/public/assets/{circle-x-BLUdeRhT.js → circle-x-BZbFCixZ.js} +1 -1
  31. package/lib/public/assets/{clock-PAhKG1jT.js → clock-BuTm35Qg.js} +1 -1
  32. package/lib/public/assets/{copy-2CHdAPBf.js → copy-W00zUBwN.js} +1 -1
  33. package/lib/public/assets/{device-explorer-EyAhEL_S.js → device-explorer-BEpS-Y-_.js} +5 -5
  34. package/lib/public/assets/{download-B1vKGhGP.js → download-CGz-MS3y.js} +1 -1
  35. package/lib/public/assets/{forgot-password-NSrrBA5N.js → forgot-password-DG7SqVgU.js} +1 -1
  36. package/lib/public/assets/{index-C8pvxG1f.css → index-DQa193-A.css} +1 -1
  37. package/lib/public/assets/{index-BEk1D5-B.js → index-DWHTp1YP.js} +1 -1
  38. package/lib/public/assets/{index-BxaH84UO.js → index-EkaQM1DK.js} +2 -2
  39. package/lib/public/assets/{input-cA_lrNIw.js → input-ChaTXeCq.js} +1 -1
  40. package/lib/public/assets/{keycodes-DZLCZ-uv.js → keycodes-BhbgeW_r.js} +10 -5
  41. package/lib/public/assets/line-chart-9P3RxScy.js +1 -0
  42. package/lib/public/assets/list-checks-_23IIEEL.js +11 -0
  43. package/lib/public/assets/{lock-ChoclF8X.js → lock-B2JaqqRI.js} +1 -1
  44. package/lib/public/assets/{login-63DiXJ0M.js → login-BEWIfLfC.js} +1 -1
  45. package/lib/public/assets/{maintenance-settings-A47Y-PYy.js → maintenance-settings-BGJIJQ-k.js} +1 -1
  46. package/lib/public/assets/{monitor-BYPF43Ds.js → monitor-BrwMY4G0.js} +1 -1
  47. package/lib/public/assets/{mouse-pointer-2-Pcl7inmZ.js → mouse-pointer-2-DsWDMyrY.js} +1 -1
  48. package/lib/public/assets/{network-ChaLfnWe.js → network-DfIhNVJv.js} +1 -1
  49. package/lib/public/assets/overview-B1mePY2_.js +6 -0
  50. package/lib/public/assets/{page-header-bv2AQSMG.js → page-header-DoD3TOzZ.js} +1 -1
  51. package/lib/public/assets/{plus-u0x2Y5uW.js → plus-BGvWvpBB.js} +1 -1
  52. package/lib/public/assets/{profile-page-Cvmef2iY.js → profile-page-CvV5aWvl.js} +1 -1
  53. package/lib/public/assets/{reset-password-DqStVhBo.js → reset-password-CGVd31LR.js} +1 -1
  54. package/lib/public/assets/{runbook-page-UK0C95QF.js → runbook-page-Bq2cvwb5.js} +1 -1
  55. package/lib/public/assets/{select-CQ6O804R.js → select-CTcD4jWR.js} +1 -1
  56. package/lib/public/assets/selector-detail-redirect-19odlD4B.js +1 -0
  57. package/lib/public/assets/selector-health-page-LqIsLrjW.js +11 -0
  58. package/lib/public/assets/session-detail-page-DQzb8_fq.js +4 -0
  59. package/lib/public/assets/{settings-Cva7V4n-.js → settings-C-EnGIqs.js} +1 -1
  60. package/lib/public/assets/stat-tile-BY0RDTRm.js +1 -0
  61. package/lib/public/assets/{tablet-BPtlTsKn.js → tablet-B4cFM-5B.js} +1 -1
  62. package/lib/public/assets/{teams-D4r1UMME.js → teams-BFe-RpYd.js} +1 -1
  63. package/lib/public/assets/time-D-gjmE1o.js +1 -0
  64. package/lib/public/assets/{trash-2-CnysPtdR.js → trash-2-CJCKBOP9.js} +1 -1
  65. package/lib/public/assets/{upload-Uyj4-Cyq.js → upload-DidWkbld.js} +1 -1
  66. package/lib/public/assets/use-builds-data-fnfxatOd.js +8 -0
  67. package/lib/public/assets/{use-password-reset-mode-Du-ykGHP.js → use-password-reset-mode-iPen3tmC.js} +1 -1
  68. package/lib/public/assets/{users-ttkwKBos.js → users-CrcjmRdf.js} +1 -1
  69. package/lib/public/assets/{users-Cs00GNmy.js → users-DBUv0CJx.js} +1 -1
  70. package/lib/public/assets/{video-off-BYa4qD8N.js → video-off-eh69-FKB.js} +1 -1
  71. package/lib/public/assets/view-state-CP9dE1UI.js +1 -0
  72. package/lib/public/assets/{webhook-settings-DuEnPlet.js → webhook-settings-CQoSshBn.js} +1 -1
  73. package/lib/public/assets/{zap-CxHn6Vwu.js → zap-CICtWiLn.js} +1 -1
  74. package/lib/public/index.html +2 -2
  75. package/lib/schema.json +5 -0
  76. package/lib/src/app/routers/dashboard.js +86 -40
  77. package/lib/src/app/routers/selector-health.js +75 -0
  78. package/lib/src/app/swagger-docs.js +77 -8
  79. package/lib/src/dashboard/event-manager.js +15 -99
  80. package/lib/src/device-managers/ios/IOSDiscoveryService.js +6 -65
  81. package/lib/src/device-managers/ios/IOSStreamService.js +7 -66
  82. package/lib/src/device-managers/ios/WDAClient.js +65 -86
  83. package/lib/src/generated/client/edge.js +27 -3
  84. package/lib/src/generated/client/index-browser.js +24 -0
  85. package/lib/src/generated/client/index.d.ts +8207 -5283
  86. package/lib/src/generated/client/index.js +27 -3
  87. package/lib/src/generated/client/package.json +1 -1
  88. package/lib/src/generated/client/schema.prisma +43 -9
  89. package/lib/src/generated/client/wasm.js +24 -0
  90. package/lib/src/prisma.js +2 -0
  91. package/lib/src/services/CleanupService.js +1 -0
  92. package/lib/src/services/NotificationService.js +3 -4
  93. package/lib/src/services/SelectorStateService.js +151 -113
  94. package/lib/src/services/SelectorVerificationJob.js +14 -9
  95. package/lib/src/services/metrics/AndroidMetricsSampler.js +94 -0
  96. package/lib/src/services/metrics/IOSMetricsSampler.js +120 -0
  97. package/lib/src/services/metrics/SessionMetricsService.js +238 -0
  98. package/lib/src/services/metrics/androidMetrics.js +94 -0
  99. package/lib/src/services/metrics/iosMetrics.js +26 -0
  100. package/lib/src/services/metrics/metricsBody.js +37 -0
  101. package/lib/src/services/metrics/types.js +19 -0
  102. package/lib/src/services/selector-health/access.js +66 -0
  103. package/lib/src/services/selector-health/healingTrend.js +49 -0
  104. package/lib/src/services/selector-health/people.js +27 -0
  105. package/lib/src/services/selector-health/selectorDetail.js +198 -0
  106. package/lib/src/services/selector-health/selectorKeys.js +36 -0
  107. package/lib/src/services/selector-health/selectorList.js +333 -0
  108. package/lib/src/services/selector-health/stateView.js +67 -0
  109. package/lib/test/helpers/scratch-database.js +2 -0
  110. package/lib/test/helpers/selector-health-fixture.js +216 -0
  111. package/lib/test/integration/team-visibility-sessions.spec.js +1 -0
  112. package/lib/test/unit/CleanupService.spec.js +28 -2
  113. package/lib/test/unit/dashboard-events-metrics.spec.js +146 -0
  114. package/lib/test/unit/healing-no-cost.spec.js +134 -0
  115. package/lib/test/unit/healing-state-endpoints.spec.js +70 -0
  116. package/lib/test/unit/healing-trend.spec.js +118 -0
  117. package/lib/test/unit/ios-discovery-same-host.spec.js +0 -1
  118. package/lib/test/unit/ios-no-network-fallback.spec.js +219 -0
  119. package/lib/test/unit/selector-event-model.spec.js +47 -0
  120. package/lib/test/unit/selector-health-actions.spec.js +106 -0
  121. package/lib/test/unit/selector-health-detail.spec.js +131 -0
  122. package/lib/test/unit/selector-health-list.spec.js +185 -0
  123. package/lib/test/unit/selector-state-service.spec.js +74 -1
  124. package/lib/test/unit/selector-verification-job.spec.js +38 -0
  125. package/lib/test/unit/session-metrics-parse.spec.js +138 -0
  126. package/lib/test/unit/session-metrics-route.spec.js +140 -0
  127. package/lib/test/unit/session-metrics-samplers.spec.js +248 -0
  128. package/lib/test/unit/session-metrics-service.spec.js +215 -0
  129. package/lib/tsconfig.tsbuildinfo +1 -1
  130. package/package.json +1 -1
  131. package/prisma/migrations/20261002120000_session_metric/migration.sql +17 -0
  132. package/prisma/migrations/20261002130000_selector_event/migration.sql +13 -0
  133. package/prisma/schema.prisma +43 -9
  134. package/schema.json +5 -0
  135. package/lib/public/assets/chevron-left-DQDpR9ne.js +0 -6
  136. package/lib/public/assets/funnel-C9mzkCSI.js +0 -6
  137. package/lib/public/assets/layers-BuXMIJz3.js +0 -6
  138. package/lib/public/assets/list-checks-B8q68tPG.js +0 -6
  139. package/lib/public/assets/overview-bj_vktQG.js +0 -1
  140. package/lib/public/assets/selector-detail-page-D-PjU6mC.js +0 -1
  141. package/lib/public/assets/selector-health-BlICzZlk.css +0 -1
  142. package/lib/public/assets/selector-health-CVm0cTOU.js +0 -16
  143. package/lib/public/assets/selector-health-page-BdibBs8g.js +0 -16
  144. package/lib/public/assets/session-detail-page-BijicGOF.js +0 -4
  145. package/lib/public/assets/stat-tile-BdIoEoou.js +0 -8
  146. package/lib/public/assets/time-B8FIgwPE.js +0 -1
  147. package/lib/src/profiling/AndroidAppProfiler.js +0 -213
  148. /package/lib/public/assets/{session-detail-page-Bzzc3bMl.css → line-chart-Bzzc3bMl.css} +0 -0
@@ -64,8 +64,6 @@ const asset_manager_1 = require("./asset-manager");
64
64
  const commands_1 = require("./commands");
65
65
  const SessionStatus_1 = require("../types/SessionStatus");
66
66
  const appium_ios_device_1 = require("appium-ios-device");
67
- const AndroidAppProfiler_1 = require("../profiling/AndroidAppProfiler");
68
- const appium_adb_1 = require("appium-adb");
69
67
  const config_1 = require("../config");
70
68
  const helpers_2 = require("../helpers");
71
69
  const typedi_1 = require("typedi");
@@ -80,6 +78,7 @@ const SocketEvents_1 = require("../enums/SocketEvents");
80
78
  const types_1 = require("../services/healing/types");
81
79
  const SelectorStateService_1 = require("../services/SelectorStateService");
82
80
  const recording_store_1 = require("../services/recording/recording-store");
81
+ const SessionMetricsService_1 = require("../services/metrics/SessionMetricsService");
83
82
  const typedi_2 = require("typedi");
84
83
  /**
85
84
  * What a Session row keeps of the capabilities the driver returned. The row
@@ -105,8 +104,6 @@ let DashboardEventManager = class DashboardEventManager {
105
104
  this.sessionToUdid = new Map();
106
105
  // Map session ID to device info
107
106
  this.sessionToDevice = new Map();
108
- // Store app profilers for Android sessions
109
- this.appProfilers = new Map();
110
107
  // Track last log line for each session (for device logs)
111
108
  this.lastLogLine = new Map();
112
109
  // Track start time for each command to calculate duration
@@ -124,8 +121,10 @@ let DashboardEventManager = class DashboardEventManager {
124
121
  };
125
122
  // create directory to store screenshots, videos and log files for the session
126
123
  (0, asset_manager_1.prepareDirectory)(session.getId());
127
- // Initialize app profiling for Android sessions
128
- const { is_profiling_available, device_info } = yield this.startAppProfiling(session.getId(), device, session.getCapabilities());
124
+ // CPU and memory for the session page's Performance panel; sampling
125
+ // starts once the session's row exists, since the samples point at it.
126
+ const metrics = typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService);
127
+ const sampled = metrics.appliesTo(device);
129
128
  // If iOS real device, start performance recording (Time Profiler)
130
129
  // Note: This only works on real devices with XCUITest driver 4.5+
131
130
  let isIosProfilingStarted = false;
@@ -149,10 +148,17 @@ let DashboardEventManager = class DashboardEventManager {
149
148
  const sessionResponse = lodash_1.default.assign({}, session.getCapabilities());
150
149
  const tracingService = typedi_1.Container.get(TracingService_1.TracingService);
151
150
  const traceId = tracingService.getTraceId(session.getId());
152
- const createData = Object.assign(Object.assign({ id: session.getId(), build: build.id ? { connect: { id: build.id } } : undefined, name: capabilities[XenonCapabilityManager_1.XENON_CAPABILITIES.SESSION_NAME] || undefined }, storedSessionCapabilities(sessionResponse)), { node_id: device.nodeId || '', has_live_video: session.getLiveVideoUrl() !== null, video_recording_enabled: capabilities[XenonCapabilityManager_1.XENON_CAPABILITIES.VIDEO_RECORDING] === true, is_profiling_available: is_profiling_available || isIosProfilingStarted, device_info: device_info ? JSON.stringify(device_info) : null, device_udid: device.udid || '', device_platform: device.platform || '', device_version: device.sdk || '', device_name: device.name, trace_id: traceId, status: 'running', api_key_id: (_a = session.apiKeyId) !== null && _a !== void 0 ? _a : null, user_id: (_b = session.userId) !== null && _b !== void 0 ? _b : null });
151
+ const createData = Object.assign(Object.assign({ id: session.getId(), build: build.id ? { connect: { id: build.id } } : undefined, name: capabilities[XenonCapabilityManager_1.XENON_CAPABILITIES.SESSION_NAME] || undefined }, storedSessionCapabilities(sessionResponse)), { node_id: device.nodeId || '', has_live_video: session.getLiveVideoUrl() !== null, video_recording_enabled: capabilities[XenonCapabilityManager_1.XENON_CAPABILITIES.VIDEO_RECORDING] === true, is_profiling_available: sampled || isIosProfilingStarted, device_udid: device.udid || '', device_platform: device.platform || '', device_version: device.sdk || '', device_name: device.name, trace_id: traceId, status: 'running', api_key_id: (_a = session.apiKeyId) !== null && _a !== void 0 ? _a : null, user_id: (_b = session.userId) !== null && _b !== void 0 ? _b : null });
153
152
  yield prisma_1.prisma.session.create({
154
153
  data: createData,
155
154
  });
155
+ if (sampled) {
156
+ metrics.start({
157
+ sessionId: session.getId(),
158
+ device,
159
+ capabilities: session.getCapabilities(),
160
+ });
161
+ }
156
162
  // Emit session started event
157
163
  void typedi_1.Container.get(SocketServer_1.SocketServer).emitToDashboardForDevices(SocketEvents_1.SocketEvents.SESSION_STARTED, Object.assign(Object.assign({}, createData), { status: 'running', build_name: buildName }), { udid: device.udid });
158
164
  // Increment Metrics
@@ -198,14 +204,14 @@ let DashboardEventManager = class DashboardEventManager {
198
204
  this.stoppingSessionIds.add(sessionId);
199
205
  try {
200
206
  logger_1.default.info(`🟢 onSessionStopped called for session ${sessionId}`);
207
+ // However the session ended, in memory or not, its sampler stops here.
208
+ yield typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService).stop(sessionId);
201
209
  // Video recording is now handled in plugin.ts deleteSession() before the session is deleted
202
210
  // This ensures we can call stop_recording_screen while the session is still active
203
211
  // Here we just handle the session status update
204
212
  const session = SessionManager_1.SESSION_MANAGER.getSession(sessionId);
205
213
  if (session) {
206
214
  logger_1.default.info(`Session ${sessionId} found in SESSION_MANAGER`);
207
- // Save Android profiling data before cleanup
208
- yield this.saveAppProfilingData(sessionId);
209
215
  // iOS profiling is now handled in plugin.ts deleteSession() before the session is deleted
210
216
  // This ensures we can call mobile: stopPerfRecord while the driver is still alive
211
217
  // Clean up syslog service for real iOS devices
@@ -689,96 +695,6 @@ let DashboardEventManager = class DashboardEventManager {
689
695
  }
690
696
  });
691
697
  }
692
- startAppProfiling(sessionId, device, sessionCapabilities) {
693
- return __awaiter(this, void 0, void 0, function* () {
694
- // Only support Android profiling
695
- if (device.platform.toLowerCase() !== 'android') {
696
- return { is_profiling_available: false };
697
- }
698
- // Check if we have an app package
699
- const appPackage = (sessionCapabilities === null || sessionCapabilities === void 0 ? void 0 : sessionCapabilities.appPackage) || (sessionCapabilities === null || sessionCapabilities === void 0 ? void 0 : sessionCapabilities['appium:appPackage']);
700
- if (!appPackage) {
701
- logger_1.default.info(`[Profiling] No app package found for session ${sessionId}, skipping profiling`);
702
- return { is_profiling_available: false };
703
- }
704
- try {
705
- // Principal Intelligence: Use the Properly configured ADB from the manager
706
- // instead of a naked instance, to avoid 'defaultArgs not iterable' crashes.
707
- const deviceManager = typedi_1.Container.get(device_managers_1.XenonManager);
708
- const androidManager = (yield deviceManager.deviceInstances()).find((m) => m instanceof AndroidDeviceManager_1.default);
709
- let adb;
710
- if (androidManager) {
711
- adb = yield androidManager.getAdbForDevice(device.udid);
712
- }
713
- else {
714
- adb = yield appium_adb_1.ADB.createADB({});
715
- }
716
- // Create app profiler
717
- const profiler = new AndroidAppProfiler_1.AndroidAppProfiler({
718
- adb,
719
- deviceUDID: device.udid,
720
- appPackage,
721
- });
722
- // Get device info and start capture
723
- const device_info = yield profiler.getDeviceInfo();
724
- yield profiler.startCapture();
725
- // Store profiler for this session
726
- this.appProfilers.set(sessionId, profiler);
727
- logger_1.default.info(`[Profiling] Started app profiling for session ${sessionId}`);
728
- return {
729
- is_profiling_available: true,
730
- device_info,
731
- };
732
- }
733
- catch (err) {
734
- logger_1.default.error(`[Profiling] Error initializing app profiler for session ${sessionId}: ${err.message}`);
735
- return { is_profiling_available: false };
736
- }
737
- });
738
- }
739
- saveAppProfilingData(sessionId) {
740
- return __awaiter(this, void 0, void 0, function* () {
741
- const profiler = this.appProfilers.get(sessionId);
742
- if (!profiler) {
743
- return;
744
- }
745
- try {
746
- // Stop capture
747
- yield profiler.stopCapture();
748
- // Get logs
749
- const logs = profiler.getLogs();
750
- if (logs.length === 0) {
751
- logger_1.default.info(`[Profiling] No profiling data to save for session ${sessionId}`);
752
- return;
753
- }
754
- // Save to database
755
- const profilingEntries = logs.map((log) => ({
756
- session_id: sessionId,
757
- cpu: log.cpu,
758
- memory: log.memory,
759
- total_cpu_used: log.total_cpu_used.toString(),
760
- total_memory_used: log.total_memory_used.toString(),
761
- raw_cpu_log: log.raw_cpu_log,
762
- raw_memory_log: log.raw_memory_log,
763
- timestamp: new Date(log.timestamp),
764
- }));
765
- // Batch insert profiling data
766
- for (const entry of profilingEntries) {
767
- yield prisma_1.prisma.profiling.create({
768
- data: entry,
769
- });
770
- }
771
- logger_1.default.info(`[Profiling] Saved ${profilingEntries.length} profiling entries for session ${sessionId}`);
772
- }
773
- catch (err) {
774
- logger_1.default.error(`[Profiling] Error saving profiling data for session ${sessionId}: ${err.message}`);
775
- }
776
- finally {
777
- // Clean up profiler
778
- this.appProfilers.delete(sessionId);
779
- }
780
- });
781
- }
782
698
  // ─── Recording (free-form mosaic) events ─────────────────────────────────
783
699
  // Emitted by RecordingOrchestrator. Distinct namespace from SESSION_*; the
784
700
  // dashboard listens to both independently. Team-scoped: a group's started
@@ -1,43 +1,10 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
2
  var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
19
3
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
20
4
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
21
5
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
22
6
  return c > 3 && r && Object.defineProperty(target, key, r), r;
23
7
  };
24
- var __importStar = (this && this.__importStar) || (function () {
25
- var ownKeys = function(o) {
26
- ownKeys = Object.getOwnPropertyNames || function (o) {
27
- var ar = [];
28
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
- return ar;
30
- };
31
- return ownKeys(o);
32
- };
33
- return function (mod) {
34
- if (mod && mod.__esModule) return mod;
35
- var result = {};
36
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
- __setModuleDefault(result, mod);
38
- return result;
39
- };
40
- })();
41
8
  var __metadata = (this && this.__metadata) || function (k, v) {
42
9
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
43
10
  };
@@ -60,10 +27,8 @@ const node_simctl_1 = __importDefault(require("node-simctl"));
60
27
  const lodash_1 = require("lodash");
61
28
  const appium_ios_device_1 = require("appium-ios-device");
62
29
  const appleIdentity_1 = require("./appleIdentity");
63
- const goIosBinary_1 = require("./goIosBinary");
64
30
  const deviceIdentity_1 = require("../deviceIdentity");
65
31
  const logger_1 = __importDefault(require("../../logger"));
66
- const fs_extra_1 = __importDefault(require("fs-extra"));
67
32
  const device_utils_1 = require("../../device-utils");
68
33
  const device_store_1 = require("../../data-service/device-store");
69
34
  const PluginContext_1 = require("../../PluginContext");
@@ -160,8 +125,7 @@ let IOSDiscoveryService = class IOSDiscoveryService {
160
125
  try {
161
126
  const existingDevice = existingDeviceDetails.find((device) => device.udid === udid && device.host === ownHost);
162
127
  if (existingDevice) {
163
- const networkIp = yield this.fetchRealDeviceNetworkIp(udid);
164
- return Object.assign(Object.assign({}, existingDevice), { ip: networkIp || (0, networkAddresses_1.sanitizeDeviceNetworkIp)(existingDevice.ip) || '' });
128
+ return Object.assign(Object.assign({}, existingDevice), { ip: (0, networkAddresses_1.sanitizeDeviceNetworkIp)(existingDevice.ip) || '' });
165
129
  }
166
130
  return yield this.getDeviceInfo(udid);
167
131
  }
@@ -192,7 +156,6 @@ let IOSDiscoveryService = class IOSDiscoveryService {
192
156
  const totalUtilizationTimeMilliSec = yield (0, device_utils_1.getUtilizationTime)(udid);
193
157
  let sdk = 'Unknown';
194
158
  let name = 'iPhone';
195
- const ipAddress = yield this.fetchRealDeviceNetworkIp(udid);
196
159
  try {
197
160
  [sdk, name] = yield Promise.all([appium_ios_device_1.utilities.getOSVersion(udid), appium_ios_device_1.utilities.getDeviceName(udid)]);
198
161
  }
@@ -211,7 +174,11 @@ let IOSDiscoveryService = class IOSDiscoveryService {
211
174
  mjpegServerPort,
212
175
  udid,
213
176
  sdk,
214
- name, ip: ipAddress, busy: false, realDevice: true, deviceType: 'real', cpuArchitecture: 'arm64', platform: (name.toLowerCase().includes('tv') ? 'tvos' : 'ios'), host: host, totalUtilizationTimeMilliSec, sessionStartTime: 0, state: (storeDevice === null || storeDevice === void 0 ? void 0 : storeDevice.state) || 'Unknown', userBlocked: (storeDevice === null || storeDevice === void 0 ? void 0 : storeDevice.userBlocked) || false }, (storeDevice || {})), (0, deviceIdentity_1.nonNullIdentity)(identity));
177
+ name,
178
+ // go-ios can't tell a phone's network address (its lockdown values
179
+ // carry none), and Xenon reaches a phone only through its forwarded
180
+ // ports, so nothing asks for it.
181
+ ip: '', busy: false, realDevice: true, deviceType: 'real', cpuArchitecture: 'arm64', platform: (name.toLowerCase().includes('tv') ? 'tvos' : 'ios'), host: host, totalUtilizationTimeMilliSec, sessionStartTime: 0, state: (storeDevice === null || storeDevice === void 0 ? void 0 : storeDevice.state) || 'Unknown', userBlocked: (storeDevice === null || storeDevice === void 0 ? void 0 : storeDevice.userBlocked) || false }, (storeDevice || {})), (0, deviceIdentity_1.nonNullIdentity)(identity));
215
182
  });
216
183
  }
217
184
  getSimulators() {
@@ -280,32 +247,6 @@ let IOSDiscoveryService = class IOSDiscoveryService {
280
247
  resolveNodeLanIp() {
281
248
  return (0, networkAddresses_1.resolveAdvertisedBindHost)(this.pluginArgs.bindHostOrIp);
282
249
  }
283
- fetchRealDeviceNetworkIp(udid) {
284
- return __awaiter(this, void 0, void 0, function* () {
285
- return (0, networkAddresses_1.sanitizeDeviceNetworkIp)(yield this.fetchIpViaGoIos(udid));
286
- });
287
- }
288
- fetchIpViaGoIos(udid) {
289
- return __awaiter(this, void 0, void 0, function* () {
290
- const goIOSPath = (0, goIosBinary_1.goIosBinaryPath)();
291
- if (!fs_extra_1.default.existsSync(goIOSPath))
292
- return '';
293
- const { exec } = yield Promise.resolve().then(() => __importStar(require('child_process')));
294
- const { promisify } = yield Promise.resolve().then(() => __importStar(require('util')));
295
- const execPromise = promisify(exec);
296
- try {
297
- const { stdout } = yield execPromise(`"${goIOSPath}" info --udid ${udid}`, {
298
- env: Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' }),
299
- });
300
- const info = JSON.parse(stdout);
301
- return info.IPAddress || '';
302
- }
303
- catch (e) {
304
- logger_1.default.debug(`Failed to fetch IP via go-ios for ${udid}: ${e}`);
305
- return '';
306
- }
307
- });
308
- }
309
250
  trackIOSDevices() {
310
251
  if (this.trackingInitialized)
311
252
  return;
@@ -401,9 +401,10 @@ let IOSStreamService = class IOSStreamService {
401
401
  });
402
402
  }
403
403
  /**
404
- * Comprehensive process-level and endpoint-level health check.
405
- * Principal Intelligence: Differentiates between 'Process dead' and 'Network unreachable'.
406
- * If only the tunnel is dead but WDA is alive via IP, it will trigger a tunnel restart.
404
+ * Comprehensive process-level and endpoint-level health check: WDA and its
405
+ * forwarders are running, and WDA answers on the stream's own port. Only
406
+ * that port: WDA names no phone, so nothing that answers elsewhere (a
407
+ * simulator's address is this Mac's own) is known to be this phone's.
407
408
  */
408
409
  isStreamResponsive(udid) {
409
410
  return __awaiter(this, void 0, void 0, function* () {
@@ -419,78 +420,18 @@ let IOSStreamService = class IOSStreamService {
419
420
  return false;
420
421
  }
421
422
  if (!isWdaIproxyAlive || !isMjpegIproxyAlive) {
422
- logger_1.default.warn(`🛡️ [${udid}] [Watchdog] Tunnel processes are dead. Attempting tunnel-only recovery...`);
423
- const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
424
- if (device && device.ip) {
425
- // Double check if WDA is alive via network IP
426
- const isWdaAccessibleViaNetwork = yield this.isWDARunningOnHost(device.ip, 8100);
427
- if (isWdaAccessibleViaNetwork) {
428
- logger_1.default.info(`🛡️ [${udid}] [Watchdog] WDA is alive on network ${device.ip}. Restarting tunnels...`);
429
- yield this.restartTunnelsOnly(session);
430
- return true; // We healed it!
431
- }
432
- }
433
- return false; // Cannot heal without network access or if WDA is also dead
423
+ logger_1.default.warn(`🛡️ [${udid}] [Watchdog] Port forwarder processes are dead.`);
424
+ return false;
434
425
  }
435
426
  // 2. Network Check: verify endpoint is responding via tunnel
436
427
  const isRespondingViaTunnel = yield this.isWDARunning(session.wdaPort);
437
428
  if (!isRespondingViaTunnel) {
438
- logger_1.default.warn(`🛡️ [${udid}] [Watchdog] WDA tunnel on port ${session.wdaPort} is unresponsive. checking network...`);
439
- const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
440
- if (device && device.ip) {
441
- const isWdaAccessibleViaNetwork = yield this.isWDARunningOnHost(device.ip, 8100);
442
- if (isWdaAccessibleViaNetwork) {
443
- logger_1.default.info(`🛡️ [${udid}] [Watchdog] WDA is alive on network but tunnel is hung. Restarting tunnels...`);
444
- yield this.restartTunnelsOnly(session);
445
- return true;
446
- }
447
- }
429
+ logger_1.default.warn(`🛡️ [${udid}] [Watchdog] WDA on port ${session.wdaPort} is unresponsive.`);
448
430
  return false;
449
431
  }
450
432
  return true;
451
433
  });
452
434
  }
453
- /**
454
- * Check if WDA is running on a specific host/port
455
- */
456
- isWDARunningOnHost(host, port) {
457
- return __awaiter(this, void 0, void 0, function* () {
458
- var _a, _b;
459
- const axios = (yield Promise.resolve().then(() => __importStar(require('axios')))).default;
460
- try {
461
- const response = yield axios.get(`http://${host}:${port}/status`, {
462
- timeout: 3000,
463
- validateStatus: (status) => status === 200,
464
- });
465
- return ((_b = (_a = response.data) === null || _a === void 0 ? void 0 : _a.value) === null || _b === void 0 ? void 0 : _b.ready) === true;
466
- }
467
- catch (e) {
468
- return false;
469
- }
470
- });
471
- }
472
- /**
473
- * Restarts only the iproxy tunnels without stopping WDA
474
- */
475
- restartTunnelsOnly(session) {
476
- return __awaiter(this, void 0, void 0, function* () {
477
- const udid = session.udid;
478
- const isolationService = typedi_2.Container.get(ResourceIsolationService_1.ResourceIsolationService);
479
- // 1. Kill old tunnels
480
- if (session.forwardWDAProcess)
481
- session.forwardWDAProcess.kill('SIGKILL');
482
- if (session.forwardMJPEGProcess)
483
- session.forwardMJPEGProcess.kill('SIGKILL');
484
- // 2. Start new tunnels
485
- const wdaIproxy = isolationService.wrapSpawn('iproxy', ['-u', udid, `${session.wdaPort}:8100`], 'Performance');
486
- const mjpegIproxy = isolationService.wrapSpawn('iproxy', ['-u', udid, `${session.mjpegPort}:9100`], 'Performance');
487
- session.forwardWDAProcess = (0, child_process_1.spawn)(wdaIproxy.command, wdaIproxy.args);
488
- typedi_2.Container.get(ProcessRegistry_1.ProcessRegistry).track({ kind: 'other', udid, process: session.forwardWDAProcess });
489
- session.forwardMJPEGProcess = (0, child_process_1.spawn)(mjpegIproxy.command, mjpegIproxy.args);
490
- typedi_2.Container.get(ProcessRegistry_1.ProcessRegistry).track({ kind: 'ios-mjpeg', udid, process: session.forwardMJPEGProcess });
491
- logger_1.default.info(`🛡️ [${udid}] [Watchdog] Tunnels restarted successfully.`);
492
- });
493
- }
494
435
  /**
495
436
  * Start streaming for a device
496
437
  */
@@ -92,96 +92,75 @@ let WDAClient = WDAClient_1 = class WDAClient {
92
92
  }
93
93
  const prefixes = (cached === null || cached === void 0 ? void 0 : cached.sessionId) ? [`/session/${cached.sessionId}`] : ['', '/session/any'];
94
94
  let lastError;
95
- // Principal Intelligence: Prioritize the host that worked last time.
96
- // Usually localhost (tunnel) is fastest, but if it's lagging, network IP is a solid fallback.
97
- const hosts = [];
98
- if (cached === null || cached === void 0 ? void 0 : cached.host) {
99
- hosts.push(cached.host);
100
- if (cached.host === '127.0.0.1' && device.ip)
101
- hosts.push(device.ip);
102
- if (cached.host !== '127.0.0.1')
103
- hosts.push('127.0.0.1');
104
- }
105
- else {
106
- hosts.push('127.0.0.1');
107
- if (device.ip)
108
- hosts.push(device.ip);
109
- }
110
- for (const targetHost of hosts) {
111
- const isNetworkHost = targetHost !== '127.0.0.1';
112
- const targetPort = isNetworkHost ? 8100 : port;
113
- for (const prefix of prefixes) {
114
- try {
115
- const isSessionless = !(options === null || options === void 0 ? void 0 : options.useSessionPath) &&
116
- (['/status', '/health', '/wda/healthcheck'].includes(endpoint));
117
- const url = `http://${targetHost}:${targetPort}${isSessionless ? '' : prefix}${endpoint}`;
118
- // Principal Stability: Screenshots, activation, and script execution need longer timeouts.
119
- // Heartbeats and Status checks should be snappy to fail-over quickly.
120
- const isHeavyCommand = endpoint.includes('screenshot') ||
121
- endpoint.includes('source') ||
122
- endpoint.includes('swipe') ||
123
- endpoint.includes('touchAndHold') ||
124
- endpoint.includes('activate') ||
125
- endpoint.includes('execute') ||
126
- endpoint.includes('actions');
127
- const timeout = isHeavyCommand ? 30000 : 5000; // Snappy 5s timeout for standard commands
128
- const res = method === 'post'
129
- ? yield axios_1.default.post(url, data || {}, { timeout })
130
- : yield axios_1.default.get(url, { timeout });
131
- // If we reached a host but it returned a logical error (e.g. 404 for session),
132
- // we should update our cache and potentially stop retrying this host/prefix combo.
133
- // However, for simplicity, we treat any successful HTTP response as "Host is alive".
134
- if (isNetworkHost && targetHost !== (cached === null || cached === void 0 ? void 0 : cached.host)) {
135
- this.log.debug(`[WDA] Successful fallback to network IP ${targetHost} for ${udid}`);
136
- }
137
- const sid = ((_a = res.data) === null || _a === void 0 ? void 0 : _a.sessionId) || ((_c = (_b = res.data) === null || _b === void 0 ? void 0 : _b.value) === null || _c === void 0 ? void 0 : _c.sessionId);
138
- if (sid && sid !== (cached === null || cached === void 0 ? void 0 : cached.sessionId)) {
139
- this.log.debug(`[WDA] Detected new session ID: ${sid} via ${targetHost}`);
140
- this.wdaConnectionCache.set(cacheKey, {
141
- host: targetHost,
142
- pathPrefix: `/session/${sid}`,
143
- sessionId: sid,
144
- });
145
- typedi_1.Container.get(IOSStreamService_1.default).setWDASessionId(udid, sid);
146
- }
147
- return res;
95
+ // Only the phone's own forwarded port. WDA names no phone, so no address
96
+ // on the network is known to be this phone's: a simulator's is this
97
+ // Mac's own, where port 8100 can be an iPhone's WDA forward.
98
+ const targetHost = '127.0.0.1';
99
+ for (const prefix of prefixes) {
100
+ try {
101
+ const isSessionless = !(options === null || options === void 0 ? void 0 : options.useSessionPath) &&
102
+ (['/status', '/health', '/wda/healthcheck'].includes(endpoint));
103
+ const url = `http://${targetHost}:${port}${isSessionless ? '' : prefix}${endpoint}`;
104
+ // Principal Stability: Screenshots, activation, and script execution need longer timeouts.
105
+ // Heartbeats and Status checks should be snappy to fail-over quickly.
106
+ const isHeavyCommand = endpoint.includes('screenshot') ||
107
+ endpoint.includes('source') ||
108
+ endpoint.includes('swipe') ||
109
+ endpoint.includes('touchAndHold') ||
110
+ endpoint.includes('activate') ||
111
+ endpoint.includes('execute') ||
112
+ endpoint.includes('actions');
113
+ const timeout = isHeavyCommand ? 30000 : 5000; // Snappy 5s timeout for standard commands
114
+ const res = method === 'post'
115
+ ? yield axios_1.default.post(url, data || {}, { timeout })
116
+ : yield axios_1.default.get(url, { timeout });
117
+ // If we reached a host but it returned a logical error (e.g. 404 for session),
118
+ // we should update our cache and potentially stop retrying this host/prefix combo.
119
+ // However, for simplicity, we treat any successful HTTP response as "Host is alive".
120
+ const sid = ((_a = res.data) === null || _a === void 0 ? void 0 : _a.sessionId) || ((_c = (_b = res.data) === null || _b === void 0 ? void 0 : _b.value) === null || _c === void 0 ? void 0 : _c.sessionId);
121
+ if (sid && sid !== (cached === null || cached === void 0 ? void 0 : cached.sessionId)) {
122
+ this.log.debug(`[WDA] Detected new session ID: ${sid} via ${targetHost}`);
123
+ this.wdaConnectionCache.set(cacheKey, {
124
+ host: targetHost,
125
+ pathPrefix: `/session/${sid}`,
126
+ sessionId: sid,
127
+ });
128
+ typedi_1.Container.get(IOSStreamService_1.default).setWDASessionId(udid, sid);
148
129
  }
149
- catch (err) {
150
- lastError = err;
151
- if (err.response) {
152
- const status = err.response.status;
153
- const errorMsg = JSON.stringify(err.response.data);
154
- this.log.info(`[WDA] Command ${method.toUpperCase()} ${endpoint} failed (${status}): ${errorMsg}`);
155
- // Principal Resiliency: Multi-Phase Recovery
156
- if (retryCount < 2) {
157
- // Phase 1: Dead Session Recovery (404)
158
- if (status === 404 && prefix.includes('/session/')) {
159
- // Special Intelligence: Some WDA versions don't support /execute, /wda/homescreen, etc.
160
- // We should NOT clear the session if these specific commands fail with 404,
161
- // as they might just be unsupported features.
162
- const NON_FATAL_ENDPOINTS = ['execute', 'homescreen', 'pressButton'];
163
- if (NON_FATAL_ENDPOINTS.some((e) => endpoint.includes(e))) {
164
- this.log.debug(`[WDA] Command ${endpoint} failed with 404 (Unsupported or Logical Error). Skipping session reset.`);
165
- throw err;
166
- }
167
- this.log.warn(`[WDA] Session ${cached === null || cached === void 0 ? void 0 : cached.sessionId} appears dead. Resetting and retrying...`);
168
- this.wdaConnectionCache.delete(cacheKey);
169
- typedi_1.Container.get(IOSStreamService_1.default).setWDASessionId(udid, '');
170
- return this.performWDACommand(udid, method, endpoint, data, retryCount + 1, options);
171
- }
172
- // Phase 2: Transient State Recovery (500)
173
- // This handles "Focused" errors, "Internal" errors, or transient locks during app switches.
174
- if (status === 500) {
175
- this.log.warn(`[WDA] Transient error (500) for ${udid}, retrying in 1s... (Reason: ${errorMsg})`);
176
- yield new Promise((r) => setTimeout(r, 1000));
177
- return this.performWDACommand(udid, method, endpoint, data, retryCount + 1, options);
130
+ return res;
131
+ }
132
+ catch (err) {
133
+ lastError = err;
134
+ if (err.response) {
135
+ const status = err.response.status;
136
+ const errorMsg = JSON.stringify(err.response.data);
137
+ this.log.info(`[WDA] Command ${method.toUpperCase()} ${endpoint} failed (${status}): ${errorMsg}`);
138
+ // Principal Resiliency: Multi-Phase Recovery
139
+ if (retryCount < 2) {
140
+ // Phase 1: Dead Session Recovery (404)
141
+ if (status === 404 && prefix.includes('/session/')) {
142
+ // Special Intelligence: Some WDA versions don't support /execute, /wda/homescreen, etc.
143
+ // We should NOT clear the session if these specific commands fail with 404,
144
+ // as they might just be unsupported features.
145
+ const NON_FATAL_ENDPOINTS = ['execute', 'homescreen', 'pressButton'];
146
+ if (NON_FATAL_ENDPOINTS.some((e) => endpoint.includes(e))) {
147
+ this.log.debug(`[WDA] Command ${endpoint} failed with 404 (Unsupported or Logical Error). Skipping session reset.`);
148
+ throw err;
178
149
  }
150
+ this.log.warn(`[WDA] Session ${cached === null || cached === void 0 ? void 0 : cached.sessionId} appears dead. Resetting and retrying...`);
151
+ this.wdaConnectionCache.delete(cacheKey);
152
+ typedi_1.Container.get(IOSStreamService_1.default).setWDASessionId(udid, '');
153
+ return this.performWDACommand(udid, method, endpoint, data, retryCount + 1, options);
154
+ }
155
+ // Phase 2: Transient State Recovery (500)
156
+ // This handles "Focused" errors, "Internal" errors, or transient locks during app switches.
157
+ if (status === 500) {
158
+ this.log.warn(`[WDA] Transient error (500) for ${udid}, retrying in 1s... (Reason: ${errorMsg})`);
159
+ yield new Promise((r) => setTimeout(r, 1000));
160
+ return this.performWDACommand(udid, method, endpoint, data, retryCount + 1, options);
179
161
  }
180
- throw err;
181
162
  }
182
- // If network host fails, don't retry other prefixes if it's just a status check
183
- if (isNetworkHost && ['/status', '/health'].includes(endpoint))
184
- break;
163
+ throw err;
185
164
  }
186
165
  }
187
166
  }