@xenon-device-management/xenon 2.13.2 → 2.14.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 (209) hide show
  1. package/README.md +11 -5
  2. package/lib/package.json +1 -1
  3. package/lib/public/assets/{AnnotationOverlay-BXruClDk.js → AnnotationOverlay-B_VNtKBH.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-qTS4LPLC.js → ApiKeyGate-BwuaMjkX.js} +1 -1
  5. package/lib/public/assets/{BugReportButton-BoGhlI7F.js → BugReportButton-BAt0_pzq.js} +1 -1
  6. package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +2 -0
  7. package/lib/public/assets/{EmptyState-Cr17PdDv.js → EmptyState-BSjhk_IV.js} +1 -1
  8. package/lib/public/assets/{FieldGroup-Bvn08svv.js → FieldGroup-Cb5QZsE1.js} +1 -1
  9. package/lib/public/assets/{FilterMenu-DDSO8aDf.js → FilterMenu-BAaSlWbW.js} +1 -1
  10. package/lib/public/assets/{Menu-B-Qn2X3T.js → Menu-JGGYrSQQ.js} +1 -1
  11. package/lib/public/assets/{Modal-Cg9V5JYU.js → Modal-CFCp3n9d.js} +1 -1
  12. package/lib/public/assets/{RecordingPage-BTUs9VTW.js → RecordingPage-CbJj_EBT.js} +1 -1
  13. package/lib/public/assets/{RecordingsPage-DYcfxITK.js → RecordingsPage-BJeYnygR.js} +2 -2
  14. package/lib/public/assets/{SegmentedControl-Zy7suhT5.js → SegmentedControl-Bg42Atjy.js} +1 -1
  15. package/lib/public/assets/{SettingCard-CWYmycoe.js → SettingCard-Njz1pUIJ.js} +1 -1
  16. package/lib/public/assets/{Table-io9fwa98.js → Table-BcTm5xV2.js} +1 -1
  17. package/lib/public/assets/{activity-Bdf_uIWl.js → activity-DOVdsfMQ.js} +1 -1
  18. package/lib/public/assets/{ai-settings-Bce5SJob.js → ai-settings-CsuJroRf.js} +2 -2
  19. package/lib/public/assets/{api-keys-CPqtVpm3.js → api-keys-BKnNSTw9.js} +1 -1
  20. package/lib/public/assets/{apps-BgZan0H9.js → apps-D0SC6uw6.js} +2 -2
  21. package/lib/public/assets/{arrow-left-B27ckB31.js → arrow-left-MMbTmgoE.js} +1 -1
  22. package/lib/public/assets/{arrow-right-DOF8ITCz.js → arrow-right-BMfnGmxW.js} +1 -1
  23. package/lib/public/assets/{arrow-up-right-eFG5KTFS.js → arrow-up-right-DuAftMWB.js} +1 -1
  24. package/lib/public/assets/{auth-shell-DlC3_27j.js → auth-shell-nZC2JNDO.js} +2 -2
  25. package/lib/public/assets/{builds-page-BuXpPfKo.js → builds-page-hDCgTwkS.js} +1 -1
  26. package/lib/public/assets/{button-fIW8owMI.js → button-Dda4irn_.js} +1 -1
  27. package/lib/public/assets/{calendar-WNjmrF-a.js → calendar-BMr0Q62v.js} +1 -1
  28. package/lib/public/assets/{check-BdTh1Sq7.js → check-CZ9iw9-e.js} +1 -1
  29. package/lib/public/assets/{chevron-right-B4BKlsqk.js → chevron-right-BRYJJVC9.js} +1 -1
  30. package/lib/public/assets/{circle-check-zJU80_EF.js → circle-check-CqAJjQ5O.js} +1 -1
  31. package/lib/public/assets/{circle-x-5Uwquo0h.js → circle-x-DlDJtnVz.js} +1 -1
  32. package/lib/public/assets/{clock-BtUprDsZ.js → clock-CpuDKysU.js} +1 -1
  33. package/lib/public/assets/{copy-DGSbZI3u.js → copy-CAQydNFe.js} +1 -1
  34. package/lib/public/assets/device-explorer-CdO6_nrn.js +282 -0
  35. package/lib/public/assets/{download-WG6l7BgD.js → download-C9XG6lLv.js} +1 -1
  36. package/lib/public/assets/{forgot-password-DnudqFFo.js → forgot-password-CGnkRgE9.js} +1 -1
  37. package/lib/public/assets/h264Stream-VQBphYhg.js +26 -0
  38. package/lib/public/assets/{index-DI1fGIyj.js → index-BbKEXUFH.js} +5 -5
  39. package/lib/public/assets/{index-HDQVe1gi.js → index-DJGc1M2f.js} +1 -1
  40. package/lib/public/assets/{input-BjDRKCea.js → input-Cm0tmSoc.js} +1 -1
  41. package/lib/public/assets/{line-chart-C0zdgd0v.js → line-chart-D2DyJCml.js} +1 -1
  42. package/lib/public/assets/{list-checks-BdW5CjM8.js → list-checks-UWf1GY99.js} +1 -1
  43. package/lib/public/assets/{lock-Dy3L4I8E.js → lock-CCJv9m_I.js} +1 -1
  44. package/lib/public/assets/{login-BHbY9CLG.js → login-f8vOVW-U.js} +1 -1
  45. package/lib/public/assets/maintenance-settings-C1PrkuAH.js +6 -0
  46. package/lib/public/assets/{monitor-8xjkeHHi.js → monitor-B2jDgwm7.js} +1 -1
  47. package/lib/public/assets/{mouse-pointer-2-CXdKMXQ8.js → mouse-pointer-2-DLI1RtzU.js} +1 -1
  48. package/lib/public/assets/{network-Cj8JOKON.js → network-ebT86qoi.js} +1 -1
  49. package/lib/public/assets/{overview-CTg7F0im.js → overview-C0aykVrd.js} +1 -1
  50. package/lib/public/assets/{page-header--aTyJXq6.js → page-header-BjzgWz-T.js} +1 -1
  51. package/lib/public/assets/{play-B-FodgHV.js → play-DLG9t8FU.js} +1 -1
  52. package/lib/public/assets/{plus-CsIk10Cq.js → plus-nBFSMu_X.js} +1 -1
  53. package/lib/public/assets/{profile-page-DkFKkMfl.js → profile-page-CSl0UYlv.js} +1 -1
  54. package/lib/public/assets/{reset-password-I2-zqLlI.js → reset-password-LeOuHqmB.js} +1 -1
  55. package/lib/public/assets/{runbook-page-DJTVCJX7.js → runbook-page-CMMeQml9.js} +1 -1
  56. package/lib/public/assets/{select-DvCSv25z.js → select-88jHcl-Z.js} +1 -1
  57. package/lib/public/assets/{selector-detail-redirect-BLVfMJs_.js → selector-detail-redirect-BbXKJjZM.js} +1 -1
  58. package/lib/public/assets/{selector-health-page-oqadRokn.js → selector-health-page-B4rA1Xkv.js} +2 -2
  59. package/lib/public/assets/session-detail-page-COL_RZxw.js +4 -0
  60. package/lib/public/assets/settings-GF6ZMSQ6.js +6 -0
  61. package/lib/public/assets/{stat-tile-DV9Ix-4O.js → stat-tile-BV98e_Sv.js} +1 -1
  62. package/lib/public/assets/{tablet-6b_SEN2X.js → tablet-BiAsnFDj.js} +1 -1
  63. package/lib/public/assets/{teams-BjZyfuHh.js → teams-B4-jrr-1.js} +1 -1
  64. package/lib/public/assets/{trash-2-CTZpPSdd.js → trash-2-C5RSuX0e.js} +1 -1
  65. package/lib/public/assets/{upload-P3XzwQUb.js → upload-BG7_rMXK.js} +1 -1
  66. package/lib/public/assets/{use-builds-data-1cOCMf-z.js → use-builds-data-J-riQYyC.js} +1 -1
  67. package/lib/public/assets/{use-password-reset-mode-DgF9w088.js → use-password-reset-mode-C8-eH-UG.js} +1 -1
  68. package/lib/public/assets/{users-DMF9Mk5H.js → users-CFba5TSy.js} +1 -1
  69. package/lib/public/assets/{users-CiNTKUiz.js → users-Cp0beJm3.js} +1 -1
  70. package/lib/public/assets/{video-off-CHp1KU_T.js → video-off-DDsEISjV.js} +1 -1
  71. package/lib/public/assets/webhook-settings-BGu32vSB.js +1 -0
  72. package/lib/public/assets/{zap-DRz0tDq5.js → zap-l4VaXzq8.js} +1 -1
  73. package/lib/public/index.html +1 -1
  74. package/lib/schema.json +19 -21
  75. package/lib/src/XenonCapabilityManager.js +22 -5
  76. package/lib/src/app/openapi/control.yaml +71 -20
  77. package/lib/src/app/openapi/grid.yaml +23 -5
  78. package/lib/src/app/openapi/platform.yaml +136 -28
  79. package/lib/src/app/routers/control.js +12 -2
  80. package/lib/src/app/routers/dashboard.js +29 -1
  81. package/lib/src/app/routers/grid.js +32 -10
  82. package/lib/src/app/routers/streamType.js +7 -2
  83. package/lib/src/app/routers/webhook.js +8 -4
  84. package/lib/src/config.js +50 -5
  85. package/lib/src/dashboard/commands.js +185 -141
  86. package/lib/src/dashboard/event-manager.js +39 -6
  87. package/lib/src/data-service/appleFamily.js +43 -0
  88. package/lib/src/data-service/db.js +11 -0
  89. package/lib/src/data-service/device-service.js +6 -1
  90. package/lib/src/data-service/device-store.js +62 -2
  91. package/lib/src/data-service/deviceClaims.js +37 -0
  92. package/lib/src/data-service/deviceFieldOwners.js +25 -11
  93. package/lib/src/data-service/deviceSettings.js +79 -0
  94. package/lib/src/data-service/prisma-store.js +150 -8
  95. package/lib/src/data-service/web-config-service.js +43 -92
  96. package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
  97. package/lib/src/device-managers/ios/WDAClient.js +44 -43
  98. package/lib/src/device-managers/shellCommands.js +96 -0
  99. package/lib/src/device-utils.js +96 -40
  100. package/lib/src/generated/client/edge.js +17 -3
  101. package/lib/src/generated/client/index-browser.js +14 -0
  102. package/lib/src/generated/client/index.d.ts +2054 -379
  103. package/lib/src/generated/client/index.js +17 -3
  104. package/lib/src/generated/client/package.json +1 -1
  105. package/lib/src/generated/client/schema.prisma +35 -7
  106. package/lib/src/generated/client/wasm.js +14 -0
  107. package/lib/src/interceptors/CommandInterceptor.js +29 -8
  108. package/lib/src/interceptors/xenonScripts.js +55 -0
  109. package/lib/src/interfaces/IPluginArgs.js +1 -2
  110. package/lib/src/plugin.js +15 -1
  111. package/lib/src/prisma.js +1 -1
  112. package/lib/src/scripts/database-check.js +124 -0
  113. package/lib/src/services/AIService.js +99 -0
  114. package/lib/src/services/CleanupService.js +6 -1
  115. package/lib/src/services/InterceptorService.js +70 -13
  116. package/lib/src/services/NetworkConditioningService.js +85 -88
  117. package/lib/src/services/NotificationService.js +36 -29
  118. package/lib/src/services/OrphanSweeper.js +5 -0
  119. package/lib/src/services/PortAllocator.js +5 -0
  120. package/lib/src/services/ServerManager.js +94 -8
  121. package/lib/src/services/SessionLifecycleService.js +47 -49
  122. package/lib/src/services/ShutdownCoordinator.js +33 -0
  123. package/lib/src/services/TeamService.js +20 -3
  124. package/lib/src/services/healing/HealingOrchestrator.js +32 -1
  125. package/lib/src/services/interceptor/AndroidProxyAdapter.js +18 -0
  126. package/lib/src/services/interceptor/interceptorOptions.js +44 -0
  127. package/lib/src/services/network/AndroidRadios.js +71 -0
  128. package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
  129. package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
  130. package/lib/src/services/network/adbForPhone.js +60 -0
  131. package/lib/src/services/network/phoneNetworkLock.js +20 -0
  132. package/lib/src/services/network/xenonProxy.js +107 -0
  133. package/lib/src/services/omni-vision/OmniVisionService.js +53 -43
  134. package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
  135. package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
  136. package/lib/src/services/omni-vision/screenScale.js +68 -0
  137. package/lib/src/services/recording/concurrency-gate.js +12 -3
  138. package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
  139. package/lib/src/services/settings/labSettings.js +149 -0
  140. package/lib/src/services/webhookEvents.js +173 -0
  141. package/lib/test/helpers/fake-adb.js +58 -0
  142. package/lib/test/helpers/fake-device-table.js +63 -24
  143. package/lib/test/helpers/scratch-database.js +1 -0
  144. package/lib/test/unit/TeamService.test.js +3 -0
  145. package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
  146. package/lib/test/unit/apple-family.spec.js +36 -0
  147. package/lib/test/unit/cleanup-settings-db.spec.js +103 -0
  148. package/lib/test/unit/cleanup-settings.spec.js +244 -0
  149. package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
  150. package/lib/test/unit/dashboard-commands.spec.js +249 -0
  151. package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
  152. package/lib/test/unit/database-check.spec.js +108 -0
  153. package/lib/test/unit/default-plugin-args.spec.js +33 -0
  154. package/lib/test/unit/device-offline-webhook.spec.js +81 -0
  155. package/lib/test/unit/device-setting-columns.spec.js +66 -0
  156. package/lib/test/unit/device-settings-loki.spec.js +87 -0
  157. package/lib/test/unit/device-settings-survive.spec.js +555 -0
  158. package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
  159. package/lib/test/unit/emulator-boot.spec.js +91 -0
  160. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +48 -0
  161. package/lib/test/unit/hub-node-same-host.spec.js +5 -0
  162. package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
  163. package/lib/test/unit/json-logging-option.spec.js +74 -0
  164. package/lib/test/unit/lab-settings.spec.js +218 -0
  165. package/lib/test/unit/max-sessions.spec.js +240 -0
  166. package/lib/test/unit/ocr-text-match.spec.js +44 -0
  167. package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
  168. package/lib/test/unit/omni-vision-ios-points.spec.js +161 -0
  169. package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
  170. package/lib/test/unit/omni-vision.spec.js +1 -1
  171. package/lib/test/unit/partial-config-file.spec.js +14 -0
  172. package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
  173. package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
  174. package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
  175. package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
  176. package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
  177. package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
  178. package/lib/test/unit/phone-network/session-start.spec.js +110 -0
  179. package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
  180. package/lib/test/unit/plugin.spec.js +3 -3
  181. package/lib/test/unit/recording-options.spec.js +181 -0
  182. package/lib/test/unit/self-healing-boot.spec.js +75 -0
  183. package/lib/test/unit/self-healing-setting.spec.js +272 -0
  184. package/lib/test/unit/session-failed-webhook.spec.js +170 -0
  185. package/lib/test/unit/shell-commands.spec.js +133 -0
  186. package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
  187. package/lib/test/unit/stream-start-pipelines.spec.js +210 -0
  188. package/lib/test/unit/stream-status-type.spec.js +4 -0
  189. package/lib/test/unit/vision-assertion.spec.js +118 -0
  190. package/lib/test/unit/wda-client-shell.spec.js +133 -0
  191. package/lib/test/unit/web-config-service.spec.js +79 -0
  192. package/lib/test/unit/webhook-delivery.spec.js +24 -0
  193. package/lib/test/unit/webhook-payloads.spec.js +273 -0
  194. package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
  195. package/lib/tsconfig.tsbuildinfo +1 -1
  196. package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
  197. package/package.json +1 -1
  198. package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
  199. package/prisma/schema.prisma +35 -7
  200. package/schema.json +19 -21
  201. package/scripts/generate-types-from-schema.js +1 -2
  202. package/scripts/lib/always-present-plugin-args.js +4 -1
  203. package/lib/public/assets/DeviceMosaicView-8J0oYqMM.js +0 -2
  204. package/lib/public/assets/device-explorer-C-KnhMHI.js +0 -279
  205. package/lib/public/assets/maintenance-settings-zDVlOMVK.js +0 -6
  206. package/lib/public/assets/recording-group-store-WVM3hoXn.js +0 -26
  207. package/lib/public/assets/session-detail-page-CGn55Pvl.js +0 -4
  208. package/lib/public/assets/settings-MsVU2ukE.js +0 -6
  209. package/lib/public/assets/webhook-settings-DiS8geIm.js +0 -1
@@ -57,7 +57,7 @@ exports.initializeStorage = initializeStorage;
57
57
  exports.getUtilizationTime = getUtilizationTime;
58
58
  exports.setUtilizationTime = setUtilizationTime;
59
59
  exports.getDeviceFiltersFromCapability = getDeviceFiltersFromCapability;
60
- exports.getBusyDevicesCount = getBusyDevicesCount;
60
+ exports.getRunningSessionsCount = getRunningSessionsCount;
61
61
  exports.updateDeviceList = updateDeviceList;
62
62
  exports.refreshSimulatorState = refreshSimulatorState;
63
63
  exports.setupCronCheckStaleDevices = setupCronCheckStaleDevices;
@@ -72,6 +72,7 @@ exports.cleanPendingSessions = cleanPendingSessions;
72
72
  exports.setupCronCleanPendingSessions = setupCronCleanPendingSessions;
73
73
  exports.setupCronCleanExpiredReservations = setupCronCleanExpiredReservations;
74
74
  exports.setupCronCleanupBuilds = setupCronCleanupBuilds;
75
+ exports.rescheduleCleanupBuilds = rescheduleCleanupBuilds;
75
76
  exports.setupCronReconcileDevices = setupCronReconcileDevices;
76
77
  exports.setupCronSweepOrphanSessions = setupCronSweepOrphanSessions;
77
78
  exports.setupCronSelectorVerification = setupCronSelectorVerification;
@@ -80,6 +81,7 @@ exports.stopAllTimers = stopAllTimers;
80
81
  const helpers_1 = require("./helpers");
81
82
  const XenonCapabilityManager_1 = require("./XenonCapabilityManager");
82
83
  const async_wait_until_1 = __importDefault(require("async-wait-until"));
84
+ const async_lock_1 = __importDefault(require("async-lock"));
83
85
  const typedi_1 = require("typedi");
84
86
  const device_managers_1 = require("./device-managers");
85
87
  const device_service_1 = require("./data-service/device-service");
@@ -152,6 +154,9 @@ function isDeviceConfigPathAbsolute(path) {
152
154
  throw new Error(`Device Config Path ${path} should be absolute`);
153
155
  }
154
156
  }
157
+ // One at a time through "is there a free slot, and which phone": see allocateDeviceForSession.
158
+ const ALLOCATION_LOCK = new async_lock_1.default();
159
+ const ALLOCATION_LOCK_KEY = 'allocate';
155
160
  // What a caller that passes no proof has: nothing. A lease then needs its token.
156
161
  const NO_LEASE_PROOF = {
157
162
  canOverride: false,
@@ -212,33 +217,42 @@ function allocateDeviceForSession(capability_1, deviceTimeOutMs_1, deviceQueryIn
212
217
  yield (0, async_wait_until_1.default)(() => __awaiter(this, void 0, void 0, function* () {
213
218
  if (abandoned)
214
219
  return false;
215
- const maxSessions = getDeviceManager().getMaxSessionCount();
216
- const busyDevicesCount = yield getBusyDevicesCount();
217
- if (maxSessions !== undefined && busyDevicesCount === maxSessions) {
218
- logger_1.default.info(`Waiting for session available, already at max session count of: ${maxSessions}`);
219
- return false;
220
- }
221
- // Get matching devices that are not reserved
222
- const candidates = (yield (0, device_service_1.getDevices)(filters)).filter((d) => !(0, device_service_1.isDeviceReserved)(d));
223
- if (candidates.length > 0) {
224
- // Principal Intelligence: Multi-node consistent locking
225
- // Atomically claim one of them: busy, with a pending claim for the
226
- // session being created (deviceClaims.ts). Every candidate, not just
227
- // the first: the lock skips a phone an SDK lease holds, and offering
228
- // it alone stalled the create while other phones were free. Exact
229
- // (udid, host) pairs: a udid repeats across hosts, and a reserved
230
- // twin of a candidate must not be locked in its place.
231
- const locked = yield store.findAndLockDevice(Object.assign(Object.assign({}, filters), { udid: [...new Set(candidates.map((d) => d.udid))] }), { claim: true, only: new Set(candidates.map((d) => `${d.udid}@${d.host}`)) });
232
- if (locked && abandoned) {
233
- yield (0, device_service_1.releasePendingClaim)(locked);
220
+ // The limit check and the claim are one step. Requests that each read
221
+ // "one slot left" before any of them claimed would all take a phone and
222
+ // run past the limit, and then no later check would see it fixed.
223
+ return ALLOCATION_LOCK.acquire(ALLOCATION_LOCK_KEY, () => __awaiter(this, void 0, void 0, function* () {
224
+ if (abandoned)
234
225
  return false;
226
+ const maxSessions = (0, deviceClaims_1.sessionCap)(getDeviceManager().getMaxSessionCount());
227
+ if (maxSessions !== undefined) {
228
+ const running = yield getRunningSessionsCount();
229
+ if (running >= maxSessions) {
230
+ logger_1.default.info(`Waiting for session available, already at max session count of: ${maxSessions}`);
231
+ return false;
232
+ }
235
233
  }
236
- device = locked;
237
- if (device !== null)
238
- return true;
239
- }
240
- logger_1.default.info(`Waiting for free device. Filter: ${JSON.stringify(filters)}}`);
241
- return false;
234
+ // Get matching devices that are not reserved
235
+ const candidates = (yield (0, device_service_1.getDevices)(filters)).filter((d) => !(0, device_service_1.isDeviceReserved)(d));
236
+ if (candidates.length > 0) {
237
+ // Principal Intelligence: Multi-node consistent locking
238
+ // Atomically claim one of them: busy, with a pending claim for the
239
+ // session being created (deviceClaims.ts). Every candidate, not just
240
+ // the first: the lock skips a phone an SDK lease holds, and offering
241
+ // it alone stalled the create while other phones were free. Exact
242
+ // (udid, host) pairs: a udid repeats across hosts, and a reserved
243
+ // twin of a candidate must not be locked in its place.
244
+ const locked = yield store.findAndLockDevice(Object.assign(Object.assign({}, filters), { udid: [...new Set(candidates.map((d) => d.udid))] }), { claim: true, only: new Set(candidates.map((d) => `${d.udid}@${d.host}`)) });
245
+ if (locked && abandoned) {
246
+ yield (0, device_service_1.releasePendingClaim)(locked);
247
+ return false;
248
+ }
249
+ device = locked;
250
+ if (device !== null)
251
+ return true;
252
+ }
253
+ logger_1.default.info(`Waiting for free device. Filter: ${JSON.stringify(filters)}}`);
254
+ return false;
255
+ }));
242
256
  }), { timeout, intervalBetweenAttempts });
243
257
  }
244
258
  catch (err) {
@@ -446,12 +460,14 @@ function getDeviceFiltersFromCapability(capability, pluginArgs) {
446
460
  if ((deviceType === null || deviceType === void 0 ? void 0 : deviceType.startsWith('real')) && pluginArgs.iosDeviceType == 'simulated') {
447
461
  throw new Error('iosDeviceType value is set to "simulated" but app provided is not suitable for simulator device.');
448
462
  }
449
- let name = undefined;
463
+ // iPad wins when both are asked for. The device store applies it; it is not
464
+ // a name match (a real phone is named by its owner), see appleFamilyOf.
465
+ let appleFamily = undefined;
450
466
  if (capability[customCapability.ipadOnly]) {
451
- name = 'iPad';
467
+ appleFamily = 'ipad';
452
468
  }
453
469
  else if (capability[customCapability.iphoneOnly]) {
454
- name = 'iPhone';
470
+ appleFamily = 'iphone';
455
471
  }
456
472
  // Ensure udid is always an array of strings for the filter
457
473
  let udidFilter = [];
@@ -469,7 +485,6 @@ function getDeviceFiltersFromCapability(capability, pluginArgs) {
469
485
  platformVersion: capability['appium:platformVersion']
470
486
  ? capability['appium:platformVersion']
471
487
  : undefined,
472
- name,
473
488
  deviceType,
474
489
  udid: udidFilter,
475
490
  busy: false,
@@ -481,8 +496,8 @@ function getDeviceFiltersFromCapability(capability, pluginArgs) {
481
496
  ? capability[customCapability.tags].split(',').map(lodash_1.default.trim)
482
497
  : undefined,
483
498
  };
484
- if (name !== undefined) {
485
- caps = Object.assign(Object.assign({}, caps), { name });
499
+ if (appleFamily !== undefined) {
500
+ caps = Object.assign(Object.assign({}, caps), { appleFamily });
486
501
  }
487
502
  return caps;
488
503
  }
@@ -492,12 +507,16 @@ function getDeviceFiltersFromCapability(capability, pluginArgs) {
492
507
  function getDeviceManager() {
493
508
  return typedi_1.Container.get(device_managers_1.XenonManager);
494
509
  }
495
- function getBusyDevicesCount() {
510
+ /**
511
+ * How many phones run an Appium session, or are being given one: what
512
+ * `maxSessions` limits. A preview, a recording or an idle SDK lease keeps a
513
+ * phone busy without being a session, so counting busy phones would be wrong
514
+ * (see `countsTowardMaxSessions`). On a hub it covers its nodes' phones too.
515
+ */
516
+ function getRunningSessionsCount() {
496
517
  return __awaiter(this, void 0, void 0, function* () {
497
518
  const allDevices = yield (0, device_service_1.getAllDevices)();
498
- return allDevices.filter((device) => {
499
- return device.busy;
500
- }).length;
519
+ return allDevices.filter(deviceClaims_1.countsTowardMaxSessions).length;
501
520
  });
502
521
  }
503
522
  function updateDeviceList(local, hubArgument, tlsRejectUnauthorized) {
@@ -629,7 +648,7 @@ function unblockCandidateDevices() {
629
648
  }
630
649
  function releaseBlockedDevices(newCommandTimeout) {
631
650
  return __awaiter(this, void 0, void 0, function* () {
632
- var _a;
651
+ var _a, _b;
633
652
  const busyDevices = yield unblockCandidateDevices();
634
653
  // A phone an SDK lease holds is the lease's to time out (its heartbeats and
635
654
  // expiry, LeaseOrphanSweeper), not idle until a session runs on it. A lease
@@ -677,6 +696,8 @@ function releaseBlockedDevices(newCommandTimeout) {
677
696
  if (timeSinceLastCmdExecuted > timeoutSeconds) {
678
697
  // unblock regardless of whether the device has session or not
679
698
  logger_1.default.info(`Unblocking device ${device.udid} at host ${device.host} because it has been idle for ${timeSinceLastCmdExecuted} seconds`);
699
+ // Before anything below can release the phone (onSessionStopped too).
700
+ yield restoreIdleSessionNetwork((_a = device.claimSessionId) !== null && _a !== void 0 ? _a : device.session_id);
680
701
  // Principal Protection: If this device has an active dashboard session, stop it properly
681
702
  if (device.session_id) {
682
703
  const sessionId = device.session_id;
@@ -692,7 +713,7 @@ function releaseBlockedDevices(newCommandTimeout) {
692
713
  }
693
714
  // Keyed on the session read here, so a release that loses the race to
694
715
  // the session's own end (and the phone's next claim) frees nothing.
695
- const claimant = (_a = device.claimSessionId) !== null && _a !== void 0 ? _a : device.session_id;
716
+ const claimant = (_b = device.claimSessionId) !== null && _b !== void 0 ? _b : device.session_id;
696
717
  yield (claimant
697
718
  ? (0, device_service_1.releaseSessionDevice)(device.udid, device.host, claimant)
698
719
  : (0, device_service_1.unblockDevice)(device.udid, device.host)).catch((err) => logger_1.default.error(`Unable to release ${device.udid}: ${err}`));
@@ -700,6 +721,24 @@ function releaseBlockedDevices(newCommandTimeout) {
700
721
  }
701
722
  });
702
723
  }
724
+ /**
725
+ * The network an idle session changed on its phone (profile, interceptor
726
+ * proxy), put back before the idle release frees the phone. Imported lazily,
727
+ * as this module's other session services are. Never throws.
728
+ */
729
+ function restoreIdleSessionNetwork(sessionId) {
730
+ return __awaiter(this, void 0, void 0, function* () {
731
+ if (!sessionId)
732
+ return;
733
+ try {
734
+ const { PhoneNetworkRestore } = yield Promise.resolve().then(() => __importStar(require('./services/network/PhoneNetworkRestore')));
735
+ yield typedi_1.Container.get(PhoneNetworkRestore).restoreSession(sessionId, 'idle timeout');
736
+ }
737
+ catch (err) {
738
+ logger_1.default.warn(`Could not put back the network of idle session ${sessionId}: ${err}`);
739
+ }
740
+ });
741
+ }
703
742
  function setupCronReleaseBlockedDevices(intervalMs, newCommandTimeoutSec) {
704
743
  return __awaiter(this, void 0, void 0, function* () {
705
744
  if (cronTimerToReleaseBlockedDevices) {
@@ -805,11 +844,15 @@ function setupCronCleanExpiredReservations(intervalMs) {
805
844
  });
806
845
  }
807
846
  /**
808
- * Sets up a cron job to purge older builds and sessions based on configuration
847
+ * Sets up a cron job to purge older builds and sessions based on configuration.
848
+ * The schedule is the one saved on the dashboard's Maintenance page, else the
849
+ * `buildCleanupSchedule` option. Calling it again replaces the job, which is
850
+ * how a schedule saved at runtime takes effect (`rescheduleCleanupBuilds`).
809
851
  */
810
852
  function setupCronCleanupBuilds(pluginArgs) {
811
853
  return __awaiter(this, void 0, void 0, function* () {
812
- const { buildCleanupSchedule = '0 0 * * *' } = pluginArgs;
854
+ const { loadEffectiveSettings } = yield Promise.resolve().then(() => __importStar(require('./services/settings/labSettings')));
855
+ const { buildCleanupSchedule } = yield loadEffectiveSettings(pluginArgs);
813
856
  const { CleanupService } = yield Promise.resolve().then(() => __importStar(require('./services/CleanupService')));
814
857
  const schedule = yield Promise.resolve().then(() => __importStar(require('node-schedule')));
815
858
  const cleanupService = typedi_1.Container.get(CleanupService);
@@ -823,6 +866,19 @@ function setupCronCleanupBuilds(pluginArgs) {
823
866
  }));
824
867
  });
825
868
  }
869
+ /**
870
+ * Put a cleanup schedule just saved on the Maintenance page to work: the old
871
+ * timer is cancelled and a new one installed. A server that runs no cleanup
872
+ * (a cloud-provider hub, see `setupMaintenanceCrons`) stays without one.
873
+ */
874
+ function rescheduleCleanupBuilds(pluginArgs) {
875
+ return __awaiter(this, void 0, void 0, function* () {
876
+ var _a;
877
+ if ((_a = pluginArgs.cloud) === null || _a === void 0 ? void 0 : _a.cloudName)
878
+ return;
879
+ yield setupCronCleanupBuilds(pluginArgs);
880
+ });
881
+ }
826
882
  let cronTimerSweepOrphanSessions;
827
883
  let cronTimerReconcileDevices;
828
884
  let cronTimerSelectorVerification;