@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
@@ -78,9 +78,12 @@ const package_json_1 = __importDefault(require("../../package.json"));
78
78
  const IPluginArgs_1 = require("../interfaces/IPluginArgs");
79
79
  const config_service_1 = require("../data-service/config-service");
80
80
  const PluginContext_1 = require("../PluginContext");
81
+ const SelfHealingSwitch_1 = require("./settings/SelfHealingSwitch");
81
82
  const device_store_1 = require("../data-service/device-store");
82
83
  const device_utils_1 = require("../device-utils");
83
84
  const localDeviceHosts_1 = require("../device-managers/localDeviceHosts");
85
+ const deviceSettings_1 = require("../data-service/deviceSettings");
86
+ const PhoneNetworkRestore_1 = require("./network/PhoneNetworkRestore");
84
87
  const app_1 = require("../app");
85
88
  const registerCommandAuth_1 = require("../app/registerCommandAuth");
86
89
  const defaultGateway_1 = require("../gateway/defaultGateway");
@@ -129,10 +132,14 @@ let ServerManager = ServerManager_1 = class ServerManager {
129
132
  this.logger.warn('Authentication is DISABLED — every /xenon/api caller is treated as a ' +
130
133
  'synthetic SUPER_ADMIN. Local development only.');
131
134
  }
135
+ this.applyRecordingOptions(pluginArgs);
132
136
  yield this.syncDatabaseAndAIConfig(pluginArgs);
133
137
  yield this.initializeCoreSubsystems(pluginArgs, cliArgs.port);
134
138
  // Before anything below starts go-ios: the reap would kill it too.
135
139
  yield this.reapLeftoverGoIos();
140
+ // The Settings page's AI self-healing toggle, which the command interceptor
141
+ // reads from memory at every command: load it before a command can arrive.
142
+ yield typedi_1.Container.get(SelfHealingSwitch_1.SelfHealingSwitch).load();
136
143
  this.registerRoutes(expressApp, httpServer, cliArgs, pluginArgs);
137
144
  yield this.bootEmulators(pluginArgs);
138
145
  this.registerDependenciesInContainer(pluginArgs, cliArgs, nodeId);
@@ -258,6 +265,9 @@ let ServerManager = ServerManager_1 = class ServerManager {
258
265
  yield (0, device_utils_1.updateDeviceList)(localHosts, pluginArgs.hub, pluginArgs.tlsRejectUnauthorized);
259
266
  // remove stale devices
260
267
  yield (0, device_utils_1.removeStaleDevices)(localHosts, pluginArgs.tlsRejectUnauthorized);
268
+ // In the background: put back the network a previous run left changed on
269
+ // this server's phones (an Offline profile, an interceptor proxy).
270
+ void this.cleanUpPhoneNetworks(localHosts, nodeId);
261
271
  this.logger.info(`🚀 Xenon will be served at http://${pluginArgs.bindHostOrIp}:${cliArgs.port}/xenon with id ${nodeId}`);
262
272
  this.logger.info('You can provide the following URLs to access Xenon:');
263
273
  for (const url of (0, networkAddresses_1.listReachableBaseUrls)(cliArgs.port)) {
@@ -266,6 +276,29 @@ let ServerManager = ServerManager_1 = class ServerManager {
266
276
  }
267
277
  });
268
278
  }
279
+ /**
280
+ * What a previous run of this server left on its phones' network: the
281
+ * changes its ledger holds, and on this server's own Android phones a proxy
282
+ * that points at a capture port of this machine where nothing answers.
283
+ * Each change is logged. A phone not connected now is put back at its next
284
+ * session here, or at the next start.
285
+ */
286
+ cleanUpPhoneNetworks(localHosts, nodeId) {
287
+ return __awaiter(this, void 0, void 0, function* () {
288
+ var _a;
289
+ try {
290
+ const devices = yield device_store_1.DeviceStoreFactory.getStore().getAllDevices();
291
+ const androids = devices
292
+ .filter((d) => { var _a; return ((_a = d.platform) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'android' && !d.cloud; })
293
+ .filter((d) => (0, localDeviceHosts_1.isOwnDevice)(localHosts, nodeId, d))
294
+ .map((d) => d.udid);
295
+ yield typedi_1.Container.get(PhoneNetworkRestore_1.PhoneNetworkRestore).cleanUpAtBoot([...new Set(androids)]);
296
+ }
297
+ catch (err) {
298
+ this.logger.warn(`Could not check the phones' network settings: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
299
+ }
300
+ });
301
+ }
269
302
  resolvePluginArgs(cliArgs) {
270
303
  return __awaiter(this, void 0, void 0, function* () {
271
304
  const pluginConfigs = cliArgs.plugin;
@@ -290,6 +323,24 @@ let ServerManager = ServerManager_1 = class ServerManager {
290
323
  return pluginArgs;
291
324
  });
292
325
  }
326
+ /**
327
+ * `maxConcurrentRecordings` and `recordingsAssetsPath` are schema options,
328
+ * but only their environment variables ever reached `config`. Apply the
329
+ * options now (option, else the variable, else the default) before the
330
+ * artifact store is built from the path and the recordings router first
331
+ * reads the cap. See `recordingConfigFrom`.
332
+ */
333
+ applyRecordingOptions(pluginArgs) {
334
+ const envCap = process.env.XENON_MAX_CONCURRENT_RECORDINGS;
335
+ const applied = (0, config_1.recordingConfigFrom)(pluginArgs);
336
+ if (pluginArgs.maxConcurrentRecordings === undefined &&
337
+ (envCap === null || envCap === void 0 ? void 0 : envCap.trim()) &&
338
+ Number(envCap) !== applied.maxConcurrentRecordings) {
339
+ this.logger.warn(`Ignoring XENON_MAX_CONCURRENT_RECORDINGS=${JSON.stringify(envCap)}: ` +
340
+ `it must be a whole number of at least 1. The cap is ${applied.maxConcurrentRecordings}.`);
341
+ }
342
+ (0, config_1.updateConfig)(applied);
343
+ }
293
344
  syncDatabaseAndAIConfig(pluginArgs) {
294
345
  return __awaiter(this, void 0, void 0, function* () {
295
346
  const { updateConfig } = yield Promise.resolve().then(() => __importStar(require('../config')));
@@ -334,11 +385,15 @@ let ServerManager = ServerManager_1 = class ServerManager {
334
385
  }
335
386
  initializeCoreSubsystems(pluginArgs, port) {
336
387
  return __awaiter(this, void 0, void 0, function* () {
388
+ // Before anything opens the database: the URL must suit the database
389
+ // client this install was built with (SQLite for the published plugin).
390
+ const { assertSupportedDatabase } = yield Promise.resolve().then(() => __importStar(require('../scripts/database-check')));
391
+ assertSupportedDatabase();
337
392
  yield (0, device_utils_1.initializeStorage)();
338
393
  const { runMigrations } = yield Promise.resolve().then(() => __importStar(require('../scripts/run-migrations')));
339
394
  yield runMigrations();
340
395
  // A hub keeps its nodes' phones: the sessions on them outlive its restart.
341
- yield device_store_1.DeviceStoreFactory.getStore().clearStorage((0, localDeviceHosts_1.devicesClearedAtBoot)(pluginArgs, (0, localDeviceHosts_1.localDeviceHosts)(pluginArgs, port)));
396
+ yield (0, deviceSettings_1.resetDevicesAtBoot)(device_store_1.DeviceStoreFactory.getStore(), pluginArgs, (0, localDeviceHosts_1.localDeviceHosts)(pluginArgs, port));
342
397
  const { bootstrapIdentity } = yield Promise.resolve().then(() => __importStar(require('./identity/bootstrap')));
343
398
  yield bootstrapIdentity();
344
399
  typedi_1.Container.get(EmailService_1.EmailService).warnIfLogFallbackEnabled();
@@ -417,16 +472,47 @@ let ServerManager = ServerManager_1 = class ServerManager {
417
472
  `${router.describeAppiumPath()}.`);
418
473
  }
419
474
  }
475
+ /**
476
+ * Boot the emulators `emulators` lists, with each one's launch options.
477
+ *
478
+ * An iOS-only server, or one told to use real Android devices only
479
+ * (`androidDeviceType: real`, which discovery would then ignore them for),
480
+ * boots none. Anything else boots them, `platform: both` included: it used
481
+ * to boot only when `platform` named Android, so with the default it did
482
+ * nothing. A boot that fails is logged and the rest go on: an emulator that
483
+ * won't start must not keep the server from coming up with the phones it
484
+ * has, and with `both` counted this step now runs on many more servers.
485
+ */
420
486
  bootEmulators(pluginArgs) {
421
487
  return __awaiter(this, void 0, void 0, function* () {
422
- if (pluginArgs.emulators &&
423
- pluginArgs.emulators.length > 0 &&
424
- pluginArgs.platform.toLowerCase().includes('android')) {
425
- this.logger.info('Emulators will be booted!!');
426
- const adb = yield appium_adb_1.ADB.createADB({});
427
- const array = pluginArgs.emulators || [];
428
- yield Promise.all(array.map((arr) => adb.launchAVD(arr.avdName, arr)));
488
+ const emulators = pluginArgs.emulators || [];
489
+ if (emulators.length === 0)
490
+ return;
491
+ const platform = String(pluginArgs.platform).toLowerCase();
492
+ if (platform === 'ios') {
493
+ this.logger.warn(`Not booting the ${emulators.length} configured emulator(s): platform is ios.`);
494
+ return;
495
+ }
496
+ if (pluginArgs.androidDeviceType === 'real') {
497
+ this.logger.warn(`Not booting the ${emulators.length} configured emulator(s): androidDeviceType is real.`);
498
+ return;
429
499
  }
500
+ this.logger.info(`Booting ${emulators.length} configured emulator(s)...`);
501
+ let adb;
502
+ try {
503
+ adb = yield appium_adb_1.ADB.createADB({});
504
+ }
505
+ catch (err) {
506
+ this.logger.warn(`Not booting the configured emulators: no usable adb (${err === null || err === void 0 ? void 0 : err.message}).`);
507
+ return;
508
+ }
509
+ const results = yield Promise.allSettled(emulators.map((emulator) => adb.launchAVD(emulator.avdName, emulator)));
510
+ results.forEach((result, index) => {
511
+ var _a, _b;
512
+ if (result.status === 'rejected') {
513
+ this.logger.warn(`Could not boot emulator ${emulators[index].avdName}: ${(_b = (_a = result.reason) === null || _a === void 0 ? void 0 : _a.message) !== null && _b !== void 0 ? _b : result.reason}`);
514
+ }
515
+ });
430
516
  });
431
517
  }
432
518
  registerDependenciesInContainer(pluginArgs, cliArgs, nodeId) {
@@ -98,6 +98,8 @@ const SessionStatus_1 = require("../types/SessionStatus");
98
98
  const SessionType_1 = __importDefault(require("../enums/SessionType"));
99
99
  const async_lock_1 = __importDefault(require("async-lock"));
100
100
  const base_driver_1 = require("@appium/base-driver");
101
+ const PhoneNetworkRestore_1 = require("./network/PhoneNetworkRestore");
102
+ const interceptorOptions_1 = require("./interceptor/interceptorOptions");
101
103
  /**
102
104
  * How long the hub waits for a node (or cloud provider) to create a session.
103
105
  * A create is sent once (InternalRequestConfig `retry: false`): the node may
@@ -877,26 +879,11 @@ let SessionLifecycleService = class SessionLifecycleService {
877
879
  applyPostSessionLogic(session, caps, device) {
878
880
  return __awaiter(this, void 0, void 0, function* () {
879
881
  const context = typedi_1.Container.get(PluginContext_1.PluginContext);
880
- const networkProfile = caps[XenonCapabilityManager_1.XENON_CAPABILITIES.NETWORK_PROFILE];
881
- if (networkProfile) {
882
- const { NetworkConditioningService } = yield Promise.resolve().then(() => __importStar(require('./NetworkConditioningService')));
883
- yield typedi_1.Container.get(NetworkConditioningService).applyProfile(session.getId(), device, networkProfile);
884
- }
885
- if (caps[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_ENABLED]) {
886
- try {
887
- const { InterceptorService } = yield Promise.resolve().then(() => __importStar(require('./InterceptorService')));
888
- yield typedi_1.Container.get(InterceptorService).start(session.getId(), device, {
889
- enabled: true,
890
- bufferSize: caps[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_BUFFER_SIZE],
891
- captureBodies: caps[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_CAPTURE_BODIES] !== false,
892
- mocks: caps[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_MOCKS] || [],
893
- includeHosts: caps[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_INCLUDE_HOSTS] || [],
894
- excludeHosts: caps[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_EXCLUDE_HOSTS] || [],
895
- });
896
- }
897
- catch (err) {
898
- this.logger.warn(`🕸️ Failed to start interceptor for ${session.getId()}: ${err.message}`);
899
- }
882
+ // Only the server that drives the phone changes its network: a node
883
+ // applies its own sessions' profile and interceptor, and a hub that did
884
+ // it too ran its own adb against the node's phone.
885
+ if (session.getType() === SessionType_1.default.LOCAL) {
886
+ yield this.applySessionNetwork(session.getId(), caps, device, context.pluginArgs);
900
887
  }
901
888
  const isDashboardEnabled = !!context.pluginArgs.enableDashboard;
902
889
  const shouldSaveLogs = session.getType() !== SessionType_1.default.CLOUD;
@@ -930,6 +917,36 @@ let SessionLifecycleService = class SessionLifecycleService {
930
917
  }
931
918
  });
932
919
  }
920
+ /**
921
+ * The session's network profile and network capture, on a phone this
922
+ * server drives. First what an earlier session left on the phone and
923
+ * couldn't put back is undone, so this session starts from the phone's own
924
+ * settings and records those as the ones to put back. Each ending undoes
925
+ * these through PhoneNetworkRestore.
926
+ */
927
+ applySessionNetwork(sessionId, caps, device, pluginArgs) {
928
+ return __awaiter(this, void 0, void 0, function* () {
929
+ var _a;
930
+ if (((_a = device.platform) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'android') {
931
+ yield typedi_1.Container.get(PhoneNetworkRestore_1.PhoneNetworkRestore).restoreLeftovers({ udid: device.udid });
932
+ }
933
+ const networkProfile = caps[XenonCapabilityManager_1.XENON_CAPABILITIES.NETWORK_PROFILE];
934
+ if (networkProfile) {
935
+ const { NetworkConditioningService } = yield Promise.resolve().then(() => __importStar(require('./NetworkConditioningService')));
936
+ yield typedi_1.Container.get(NetworkConditioningService).applyProfile(sessionId, device, networkProfile);
937
+ }
938
+ const interceptor = (0, interceptorOptions_1.resolveInterceptorOptions)(caps, pluginArgs.interceptor);
939
+ if (interceptor.enabled) {
940
+ try {
941
+ const { InterceptorService } = yield Promise.resolve().then(() => __importStar(require('./InterceptorService')));
942
+ yield typedi_1.Container.get(InterceptorService).start(sessionId, device, interceptor);
943
+ }
944
+ catch (err) {
945
+ this.logger.warn(`🕸️ Failed to start interceptor for ${sessionId}: ${err.message}`);
946
+ }
947
+ }
948
+ });
949
+ }
933
950
  /**
934
951
  * The row a hub keeps for a session it routes to a node or a cloud when the
935
952
  * dashboard's own (onSessionStarted) isn't written: with the dashboard off,
@@ -1097,6 +1114,10 @@ let SessionLifecycleService = class SessionLifecycleService {
1097
1114
  // which releases ports and archives video twice.
1098
1115
  if (sessionId) {
1099
1116
  yield sessionCleanupLock.acquire(sessionId, () => __awaiter(this, void 0, void 0, function* () {
1117
+ // The phone's network (profile, interceptor proxy) and the capture,
1118
+ // before the phone is released to the next session. Whether or not
1119
+ // SESSION_MANAGER holds the session.
1120
+ yield typedi_1.Container.get(PhoneNetworkRestore_1.PhoneNetworkRestore).restoreSession(sessionId, 'session deleted');
1100
1121
  yield (0, device_service_1.releaseSessionDevices)(sessionId);
1101
1122
  this.logger.info(`📱 Unblocking the device that is blocked for session ${sessionId}`);
1102
1123
  const session = SessionManager_1.SESSION_MANAGER.getSession(sessionId);
@@ -1137,37 +1158,10 @@ let SessionLifecycleService = class SessionLifecycleService {
1137
1158
  // Another concurrent deleteSession already cleaned up.
1138
1159
  return;
1139
1160
  }
1140
- const device = session.getDevice();
1141
- try {
1142
- const { NetworkConditioningService } = yield Promise.resolve().then(() => __importStar(require('./NetworkConditioningService')));
1143
- yield typedi_1.Container.get(NetworkConditioningService).reset(sessionId, device);
1144
- }
1145
- catch (resetErr) {
1146
- this.logger.warn(`⚠️ NetworkConditioningService.reset failed for session ${sessionId}: ${resetErr.message}`);
1147
- }
1148
- try {
1149
- const { InterceptorService } = yield Promise.resolve().then(() => __importStar(require('./InterceptorService')));
1150
- const interceptor = typedi_1.Container.get(InterceptorService);
1151
- if (interceptor.isActive(sessionId))
1152
- yield interceptor.stop(sessionId);
1153
- }
1154
- catch (interceptorErr) {
1155
- this.logger.warn(`⚠️ InterceptorService.stop failed for session ${sessionId}: ${interceptorErr.message}`);
1156
- }
1161
+ // The phone's network was put back before it was released (above).
1157
1162
  yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStopped(sessionId, status, reason);
1158
1163
  SessionManager_1.SESSION_MANAGER.removeSession(sessionId);
1159
1164
  typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer).forget(sessionId);
1160
- try {
1161
- const { getSessionById } = yield Promise.resolve().then(() => __importStar(require('../dashboard/services/session-service')));
1162
- const sessionData = yield getSessionById(sessionId);
1163
- if (sessionData && (sessionData.status === 'failed' || sessionData.failure_reason)) {
1164
- const { NotificationService } = yield Promise.resolve().then(() => __importStar(require('./NotificationService')));
1165
- yield typedi_1.Container.get(NotificationService).dispatchEvent('session_failed', sessionData);
1166
- }
1167
- }
1168
- catch (err) {
1169
- /* ignore notification errors */
1170
- }
1171
1165
  }));
1172
1166
  }
1173
1167
  }
@@ -1240,6 +1234,7 @@ let SessionLifecycleService = class SessionLifecycleService {
1240
1234
  stopSessionForShutdown(sessionId, reason) {
1241
1235
  return __awaiter(this, void 0, void 0, function* () {
1242
1236
  yield sessionCleanupLock.acquire(sessionId, () => __awaiter(this, void 0, void 0, function* () {
1237
+ yield typedi_1.Container.get(PhoneNetworkRestore_1.PhoneNetworkRestore).restoreSession(sessionId, reason);
1243
1238
  try {
1244
1239
  yield (0, device_service_1.releaseSessionDevices)(sessionId);
1245
1240
  }
@@ -1258,7 +1253,10 @@ let SessionLifecycleService = class SessionLifecycleService {
1258
1253
  }
1259
1254
  }
1260
1255
  try {
1261
- yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStopped(sessionId, SessionStatus_1.SessionStatus.FAILED, reason);
1256
+ // A shutdown fails the sessions it drains; no test caused it, so no webhook.
1257
+ yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStopped(sessionId, SessionStatus_1.SessionStatus.FAILED, reason, {
1258
+ notify: false,
1259
+ });
1262
1260
  }
1263
1261
  catch (err) {
1264
1262
  this.logger.warn(`[shutdown] onSessionStopped failed for ${sessionId}: ${err.message}`);
@@ -24,6 +24,7 @@ const logger_1 = __importDefault(require("../logger"));
24
24
  const SessionManager_1 = require("../sessions/SessionManager");
25
25
  const SessionLifecycleService_1 = require("./SessionLifecycleService");
26
26
  const SessionType_1 = __importDefault(require("../enums/SessionType"));
27
+ const PhoneNetworkRestore_1 = require("./network/PhoneNetworkRestore");
27
28
  // Graceful shutdown phase that runs BEFORE the existing infrastructure
28
29
  // teardown in index.ts:cleanup(). Active sessions get a bounded chance to
29
30
  // archive their video + release ports + mark themselves failed, so we don't
@@ -57,6 +58,7 @@ let ShutdownCoordinator = class ShutdownCoordinator {
57
58
  const sessions = SessionManager_1.SESSION_MANAGER.getAllSessions().filter((session) => session.getType() === SessionType_1.default.LOCAL);
58
59
  if (sessions.length === 0) {
59
60
  this.logger.info('No active sessions to drain');
61
+ yield this.restorePhoneNetworks(timeoutMs);
60
62
  return { attempted: 0, completed: 0 };
61
63
  }
62
64
  this.logger.info(`Draining ${sessions.length} active session(s) (overall=${timeoutMs}ms, per-session=${perSessionTimeoutMs}ms)`);
@@ -84,10 +86,41 @@ let ShutdownCoordinator = class ShutdownCoordinator {
84
86
  }));
85
87
  const results = yield Promise.allSettled(tasks);
86
88
  const completed = results.filter((r) => r.status === 'fulfilled' && r.value).length;
89
+ yield this.restorePhoneNetworks(Math.max(0, deadline - Date.now()));
87
90
  this.logger.info(`Drain complete: ${completed}/${sessions.length} session(s) finalized`);
88
91
  return { attempted: sessions.length, completed };
89
92
  });
90
93
  }
94
+ /**
95
+ * The network of every phone a session of this server still has changed
96
+ * (profile, interceptor proxy), sessions SESSION_MANAGER doesn't hold
97
+ * included, within what is left of the budget. A shutdown that never gets
98
+ * here (Appium exits on SIGTERM first) leaves it to the next start, from
99
+ * the ledger (PhoneNetworkRestore.cleanUpAtBoot).
100
+ */
101
+ restorePhoneNetworks(budgetMs) {
102
+ return __awaiter(this, void 0, void 0, function* () {
103
+ var _a;
104
+ if (budgetMs <= 0)
105
+ return;
106
+ let timer;
107
+ try {
108
+ yield Promise.race([
109
+ typedi_1.Container.get(PhoneNetworkRestore_1.PhoneNetworkRestore).restoreAll('Xenon is shutting down'),
110
+ new Promise((resolve) => {
111
+ timer = setTimeout(resolve, budgetMs);
112
+ }),
113
+ ]);
114
+ }
115
+ catch (err) {
116
+ this.logger.warn(`[shutdown] Putting phone networks back failed: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
117
+ }
118
+ finally {
119
+ if (timer)
120
+ clearTimeout(timer);
121
+ }
122
+ });
123
+ }
91
124
  };
92
125
  exports.ShutdownCoordinator = ShutdownCoordinator;
93
126
  exports.ShutdownCoordinator = ShutdownCoordinator = __decorate([
@@ -52,20 +52,37 @@ let TeamService = class TeamService {
52
52
  delete(id) {
53
53
  return __awaiter(this, void 0, void 0, function* () {
54
54
  // Apps too: App.teamId is onDelete SetNull, so deleting a team that owns
55
- // apps would quietly share them with every member.
56
- const [devices, members, apps] = yield Promise.all([
55
+ // apps would quietly share them with every member. So is a phone's saved
56
+ // team (DeviceSetting): a phone of the team that is not connected now
57
+ // would come back in the shared pool.
58
+ const [connected, saved, members, apps] = yield Promise.all([
57
59
  prisma_1.prisma.device.count({ where: { teamId: id } }),
60
+ prisma_1.prisma.deviceSetting.findMany({ where: { teamId: id }, select: { udid: true, host: true } }),
58
61
  prisma_1.prisma.teamMember.count({ where: { teamId: id } }),
59
62
  prisma_1.prisma.app.count({ where: { teamId: id } }),
60
63
  ]);
64
+ const away = yield this.notConnected(saved);
65
+ const devices = connected + away;
61
66
  if (devices > 0 || members > 0 || apps > 0) {
62
- throw new Error(`Team still has ${devices} device(s), ${members} member(s) and ${apps} app(s). Reassign them before deleting.`);
67
+ const phones = away > 0 ? `${devices} device(s) (${away} not connected now)` : `${devices} device(s)`;
68
+ throw new Error(`Team still has ${phones}, ${members} member(s) and ${apps} app(s). Reassign them before deleting.`);
63
69
  }
64
70
  // Clear teamId on any (revoked) ApiKey rows so the FK doesn't block delete.
65
71
  yield prisma_1.prisma.apiKey.updateMany({ where: { teamId: id }, data: { teamId: null } });
66
72
  yield prisma_1.prisma.team.delete({ where: { id } });
67
73
  });
68
74
  }
75
+ /** How many of these saved phones have no Device row: not connected now. */
76
+ notConnected(saved) {
77
+ return __awaiter(this, void 0, void 0, function* () {
78
+ if (saved.length === 0)
79
+ return 0;
80
+ const here = yield prisma_1.prisma.device.count({
81
+ where: { OR: saved.map(({ udid, host }) => ({ udid, host })) },
82
+ });
83
+ return saved.length - here;
84
+ });
85
+ }
69
86
  listMembers(teamId) {
70
87
  return __awaiter(this, void 0, void 0, function* () {
71
88
  const rows = yield prisma_1.prisma.teamMember.findMany({
@@ -70,6 +70,7 @@ const ResilioTreeHealingProvider_1 = require("./ResilioTreeHealingProvider");
70
70
  const HealingMetrics_1 = require("./HealingMetrics");
71
71
  const attributes_1 = require("../telemetry/attributes");
72
72
  const xenonOptions_1 = require("../session/xenonOptions");
73
+ const screenScale_1 = require("../omni-vision/screenScale");
73
74
  // §2.7 healing-tier capability gate. The tier numbering here is the
74
75
  // externally-facing capability contract (xe:options.healingTiers),
75
76
  // which is array-position based: providers[i] is tier i+1 (1=Resilio,
@@ -158,7 +159,10 @@ let HealingOrchestrator = class HealingOrchestrator {
158
159
  span.addEvent('tier_started', { tier: provider.name });
159
160
  try {
160
161
  this.logger.info(`Attempting Tier ${provider.tier}: ${provider.name}...`);
161
- const result = yield provider.heal(context);
162
+ let result = yield provider.heal(context);
163
+ if (result === null || result === void 0 ? void 0 : result.rect) {
164
+ result = yield this.rectInDriverCoordinates(result, result.rect, context, provider);
165
+ }
162
166
  HealingMetrics_1.HEALING_METRICS.record(provider.tier, provider.name, result ? 'success' : 'failure', Date.now() - tierStart);
163
167
  if (result) {
164
168
  this.logger.info(`✨ Provider ${provider.name} found a match! Confidence: ${(result.confidence * 100).toFixed(0)}%`);
@@ -267,6 +271,33 @@ let HealingOrchestrator = class HealingOrchestrator {
267
271
  return null;
268
272
  });
269
273
  }
274
+ /**
275
+ * The OCR and Visual AI tiers find the element in the screenshot, so their
276
+ * `rect` is in its pixels. The interceptor taps it, and asks iOS for the
277
+ * element there, in the driver's coordinates (points on iOS), so it is
278
+ * converted here. If that can't be worked out on iOS, a virtual element
279
+ * (only a position) is no use and the tier counts as failed; a real element
280
+ * the tier resolved keeps its id and loses only the rect.
281
+ */
282
+ rectInDriverCoordinates(result, rect, context, provider) {
283
+ return __awaiter(this, void 0, void 0, function* () {
284
+ var _a, _b;
285
+ try {
286
+ const scale = yield (0, screenScale_1.screenScaleOf)(context.driver, (_a = context.screenshotBase64) !== null && _a !== void 0 ? _a : '');
287
+ return Object.assign(Object.assign({}, result), { rect: (0, screenScale_1.toDriverRect)(rect, scale) });
288
+ }
289
+ catch (err) {
290
+ const reason = (_b = err === null || err === void 0 ? void 0 : err.message) !== null && _b !== void 0 ? _b : err;
291
+ if (!result.id.startsWith('healed_')) {
292
+ this.logger.warn(`${provider.name}: the element's position is unknown (${reason}).`);
293
+ return Object.assign(Object.assign({}, result), { rect: undefined });
294
+ }
295
+ this.logger.warn(`${provider.name} found the element in the screenshot, but not where it is on the ` +
296
+ `screen (${reason}); trying the next tier.`);
297
+ return null;
298
+ }
299
+ });
300
+ }
270
301
  };
271
302
  exports.HealingOrchestrator = HealingOrchestrator;
272
303
  exports.HealingOrchestrator = HealingOrchestrator = __decorate([
@@ -10,6 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.AndroidProxyAdapter = void 0;
13
+ const xenonProxy_1 = require("../network/xenonProxy");
13
14
  class AndroidProxyAdapter {
14
15
  constructor(getAdb) {
15
16
  this.getAdb = getAdb;
@@ -38,6 +39,23 @@ class AndroidProxyAdapter {
38
39
  yield adb.adbExec(['-s', udid, 'shell', 'settings', 'put', 'global', 'http_proxy', ':0']);
39
40
  });
40
41
  }
42
+ /** The phone's global HTTP proxy as stored: `host:port`, or `null` / `:0` / '' for none. */
43
+ readProxy(udid) {
44
+ return __awaiter(this, void 0, void 0, function* () {
45
+ const adb = yield this.getAdb(udid);
46
+ const out = yield adb.adbExec(['-s', udid, 'shell', 'settings', 'get', 'global', 'http_proxy']);
47
+ return String(out !== null && out !== void 0 ? out : '').trim();
48
+ });
49
+ }
50
+ /** Puts `previous` back as the phone's proxy, or clears it when there was none. */
51
+ restoreProxy(udid, previous) {
52
+ return __awaiter(this, void 0, void 0, function* () {
53
+ if (previous === null || (0, xenonProxy_1.isUnsetProxy)(previous))
54
+ return this.clearProxy(udid);
55
+ const adb = yield this.getAdb(udid);
56
+ yield adb.adbExec(['-s', udid, 'shell', 'settings', 'put', 'global', 'http_proxy', previous]);
57
+ });
58
+ }
41
59
  // Forwards a port on the device back to a port on the host over the adb transport
42
60
  // (USB or wireless adb). The device sees `127.0.0.1:devicePort` and that traffic
43
61
  // reaches the host's `hostPort`. This is the standard way to make a host-side
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveInterceptorOptions = resolveInterceptorOptions;
4
+ const XenonCapabilityManager_1 = require("../../XenonCapabilityManager");
5
+ /**
6
+ * The network capture a session gets: its own capability (any of the shapes
7
+ * getXenonCapabilities reads) wins field by field, and the server's
8
+ * `interceptor` option fills in what the session leaves unset.
9
+ *
10
+ * - `enabled`: the session's, when it says true or false; else the server's;
11
+ * else off.
12
+ * - `bufferSize`, `captureBodies`: the session's, else the server's, else the
13
+ * service's defaults (1000, true).
14
+ * - `mocks`, `includeHosts`, `excludeHosts`: the session's only.
15
+ *
16
+ * `sessionCaps` is getXenonCapabilities' output, where a field the session
17
+ * didn't set is `undefined`.
18
+ */
19
+ function resolveInterceptorOptions(sessionCaps, server) {
20
+ var _a;
21
+ const own = (key) => sessionCaps[key];
22
+ const enabled = own(XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_ENABLED);
23
+ const bufferSize = own(XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_BUFFER_SIZE);
24
+ const captureBodies = own(XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_CAPTURE_BODIES);
25
+ return {
26
+ enabled: typeof enabled === 'boolean' ? enabled : (server === null || server === void 0 ? void 0 : server.enabled) === true,
27
+ bufferSize: (_a = positive(bufferSize)) !== null && _a !== void 0 ? _a : positive(server === null || server === void 0 ? void 0 : server.bufferSize),
28
+ captureBodies: typeof captureBodies === 'boolean'
29
+ ? captureBodies
30
+ : typeof (server === null || server === void 0 ? void 0 : server.captureBodies) === 'boolean'
31
+ ? server.captureBodies
32
+ : true,
33
+ mocks: listOr(own(XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_MOCKS)),
34
+ includeHosts: listOr(own(XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_INCLUDE_HOSTS)),
35
+ excludeHosts: listOr(own(XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_EXCLUDE_HOSTS)),
36
+ };
37
+ }
38
+ function positive(value) {
39
+ const n = typeof value === 'string' && value.trim() !== '' ? Number(value) : value;
40
+ return typeof n === 'number' && Number.isFinite(n) && n > 0 ? n : undefined;
41
+ }
42
+ function listOr(value) {
43
+ return Array.isArray(value) ? value : [];
44
+ }
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.AndroidRadios = void 0;
13
+ /**
14
+ * A phone's Wi-Fi and mobile data, through the resolved adb (`adbExec`: no
15
+ * shell, no bare `adb`).
16
+ */
17
+ class AndroidRadios {
18
+ constructor(getAdb) {
19
+ this.getAdb = getAdb;
20
+ }
21
+ /**
22
+ * `wifi_on` is 1 or 2 when Wi-Fi is on (2: on in spite of airplane mode),
23
+ * 0 or 3 when it is off. `mobile_data` is 1 or 0. Anything else, or a read
24
+ * that fails, is null: "don't know".
25
+ */
26
+ read(udid) {
27
+ return __awaiter(this, void 0, void 0, function* () {
28
+ const adb = yield this.getAdb(udid);
29
+ const get = (name) => __awaiter(this, void 0, void 0, function* () {
30
+ try {
31
+ const out = yield adb.adbExec(['-s', udid, 'shell', 'settings', 'get', 'global', name]);
32
+ return String(out !== null && out !== void 0 ? out : '').trim();
33
+ }
34
+ catch (_a) {
35
+ return null;
36
+ }
37
+ });
38
+ const wifi = yield get('wifi_on');
39
+ const data = yield get('mobile_data');
40
+ return {
41
+ wifiOn: wifi === '1' || wifi === '2' ? true : wifi === '0' || wifi === '3' ? false : null,
42
+ dataOn: data === '1' ? true : data === '0' ? false : null,
43
+ };
44
+ });
45
+ }
46
+ /** Mobile data, then Wi-Fi, off. */
47
+ turnOff(udid) {
48
+ return __awaiter(this, void 0, void 0, function* () {
49
+ const adb = yield this.getAdb(udid);
50
+ yield adb.adbExec(['-s', udid, 'shell', 'svc', 'data', 'disable']);
51
+ yield adb.adbExec(['-s', udid, 'shell', 'svc', 'wifi', 'disable']);
52
+ });
53
+ }
54
+ /** Turns on the ones asked for; leaves the others as they are. */
55
+ turnOn(udid, which) {
56
+ return __awaiter(this, void 0, void 0, function* () {
57
+ if (!which.data && !which.wifi)
58
+ return;
59
+ const adb = yield this.getAdb(udid);
60
+ if (which.data)
61
+ yield adb.adbExec(['-s', udid, 'shell', 'svc', 'data', 'enable']);
62
+ if (which.wifi)
63
+ yield adb.adbExec(['-s', udid, 'shell', 'svc', 'wifi', 'enable']);
64
+ });
65
+ }
66
+ /** What to turn back on after Offline: what was on, and what nobody knew about. */
67
+ static toRestore(prior) {
68
+ return { data: prior.dataOn !== false, wifi: prior.wifiOn !== false };
69
+ }
70
+ }
71
+ exports.AndroidRadios = AndroidRadios;