@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
@@ -0,0 +1,184 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
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;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
9
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
10
+ return new (P || (P = Promise))(function (resolve, reject) {
11
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
12
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
13
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
14
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
15
+ });
16
+ };
17
+ var __importDefault = (this && this.__importDefault) || function (mod) {
18
+ return (mod && mod.__esModule) ? mod : { "default": mod };
19
+ };
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.PhoneNetworkLedger = void 0;
22
+ const typedi_1 = require("typedi");
23
+ const async_lock_1 = __importDefault(require("async-lock"));
24
+ const prisma_1 = require("../../prisma");
25
+ const logger_1 = __importDefault(require("../../logger"));
26
+ const PREFIX = 'phone-network:';
27
+ /**
28
+ * Xenon's record of the network changes it made to its phones, kept until
29
+ * each is put back. In memory a session's changes are undone when it ends; a
30
+ * crash or a kill -9 forgets them, and the phone stays offline or pointed at
31
+ * a proxy nobody serves. At its next start the server reads this record and
32
+ * puts those phones back (PhoneNetworkRestore).
33
+ *
34
+ * Kept in this server's own database, which is what makes it this server's:
35
+ * a hub and a node on one machine see the same Android phones, but each has
36
+ * its own database. It has no table of its own: WebConfig is the database's
37
+ * key-value table (MetricsService keeps its counters there), and a new table
38
+ * would mean a migration and a regenerated client for a handful of rows. One
39
+ * row per session, `phone-network:<sessionId>`, holding the JSON above.
40
+ *
41
+ * A failed write is logged and never fails a session: the change is still
42
+ * made, and undone when the session ends; only a crash before then would
43
+ * leave it.
44
+ */
45
+ let PhoneNetworkLedger = class PhoneNetworkLedger {
46
+ constructor() {
47
+ this.logger = logger_1.default.scope('PhoneNetwork');
48
+ this.lock = new async_lock_1.default();
49
+ }
50
+ /** Adds `part` to the session's record, creating the record if needed. */
51
+ note(sessionId, udid, part) {
52
+ return __awaiter(this, void 0, void 0, function* () {
53
+ yield this.guard(`record a network change for ${udid} (session ${sessionId})`, () => this.lock.acquire(sessionId, () => __awaiter(this, void 0, void 0, function* () {
54
+ const current = yield this.read(sessionId);
55
+ const next = Object.assign(Object.assign({}, (current !== null && current !== void 0 ? current : { sessionId, udid, at: Date.now() })), part);
56
+ yield this.write(next);
57
+ })));
58
+ });
59
+ }
60
+ /** The part was put back: drops it, and the record once nothing is left. */
61
+ settle(sessionId, part) {
62
+ return __awaiter(this, void 0, void 0, function* () {
63
+ yield this.guard(`settle the ${part} change of session ${sessionId}`, () => this.lock.acquire(sessionId, () => __awaiter(this, void 0, void 0, function* () {
64
+ const current = yield this.read(sessionId);
65
+ if (!current)
66
+ return;
67
+ const next = Object.assign({}, current);
68
+ delete next[part];
69
+ if (next.offline || next.proxy)
70
+ yield this.write(next);
71
+ else
72
+ yield this.remove(sessionId);
73
+ })));
74
+ });
75
+ }
76
+ /** Drops the session's record, whatever it holds. */
77
+ forget(sessionId) {
78
+ return __awaiter(this, void 0, void 0, function* () {
79
+ yield this.guard(`forget the network changes of session ${sessionId}`, () => this.lock.acquire(sessionId, () => this.remove(sessionId)));
80
+ });
81
+ }
82
+ /** The session's record, or null. */
83
+ get(sessionId) {
84
+ return __awaiter(this, void 0, void 0, function* () {
85
+ var _a;
86
+ try {
87
+ return yield this.read(sessionId);
88
+ }
89
+ catch (err) {
90
+ this.logger.warn(`Could not read the phone network record: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
91
+ return null;
92
+ }
93
+ });
94
+ }
95
+ /** Every record, oldest first. A row that isn't a record is dropped. */
96
+ list() {
97
+ return __awaiter(this, void 0, void 0, function* () {
98
+ var _a;
99
+ let rows = [];
100
+ try {
101
+ rows = yield prisma_1.prisma.webConfig.findMany({
102
+ where: { name: { startsWith: PREFIX } },
103
+ select: { name: true, value: true },
104
+ });
105
+ }
106
+ catch (err) {
107
+ this.logger.warn(`Could not read the phone network record: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
108
+ return [];
109
+ }
110
+ const out = [];
111
+ for (const row of rows) {
112
+ const change = parseChange(row.value);
113
+ if (change)
114
+ out.push(change);
115
+ else
116
+ yield this.remove(row.name.slice(PREFIX.length)).catch(() => undefined);
117
+ }
118
+ return out.sort((a, b) => a.at - b.at);
119
+ });
120
+ }
121
+ read(sessionId) {
122
+ return __awaiter(this, void 0, void 0, function* () {
123
+ const row = yield prisma_1.prisma.webConfig.findUnique({ where: { name: PREFIX + sessionId } });
124
+ return row ? parseChange(row.value) : null;
125
+ });
126
+ }
127
+ write(change) {
128
+ return __awaiter(this, void 0, void 0, function* () {
129
+ const name = PREFIX + change.sessionId;
130
+ const value = JSON.stringify(change);
131
+ yield prisma_1.prisma.webConfig.upsert({
132
+ where: { name },
133
+ update: { value },
134
+ create: { id: name, name, value },
135
+ });
136
+ });
137
+ }
138
+ remove(sessionId) {
139
+ return __awaiter(this, void 0, void 0, function* () {
140
+ yield prisma_1.prisma.webConfig.deleteMany({ where: { name: PREFIX + sessionId } });
141
+ });
142
+ }
143
+ guard(what, fn) {
144
+ return __awaiter(this, void 0, void 0, function* () {
145
+ var _a;
146
+ try {
147
+ yield fn();
148
+ }
149
+ catch (err) {
150
+ this.logger.warn(`Could not ${what}: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
151
+ }
152
+ });
153
+ }
154
+ };
155
+ exports.PhoneNetworkLedger = PhoneNetworkLedger;
156
+ exports.PhoneNetworkLedger = PhoneNetworkLedger = __decorate([
157
+ (0, typedi_1.Service)()
158
+ ], PhoneNetworkLedger);
159
+ function parseChange(value) {
160
+ try {
161
+ const v = JSON.parse(value);
162
+ if (!v || typeof v.sessionId !== 'string' || typeof v.udid !== 'string')
163
+ return null;
164
+ if (typeof v.at !== 'number')
165
+ return null;
166
+ const change = { sessionId: v.sessionId, udid: v.udid, at: v.at };
167
+ if (v.offline && typeof v.offline === 'object') {
168
+ change.offline = {
169
+ wifiOn: boolOrNull(v.offline.wifiOn),
170
+ dataOn: boolOrNull(v.offline.dataOn),
171
+ };
172
+ }
173
+ if (v.proxy && typeof v.proxy.set === 'string') {
174
+ change.proxy = Object.assign({ set: v.proxy.set, previous: typeof v.proxy.previous === 'string' ? v.proxy.previous : null }, (typeof v.proxy.reversePort === 'number' ? { reversePort: v.proxy.reversePort } : {}));
175
+ }
176
+ return change.offline || change.proxy ? change : null;
177
+ }
178
+ catch (_a) {
179
+ return null;
180
+ }
181
+ }
182
+ function boolOrNull(v) {
183
+ return typeof v === 'boolean' ? v : null;
184
+ }
@@ -0,0 +1,330 @@
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
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
19
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
20
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
21
+ 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
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
23
+ };
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
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
42
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
43
+ return new (P || (P = Promise))(function (resolve, reject) {
44
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
45
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
46
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
47
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
48
+ });
49
+ };
50
+ var __importDefault = (this && this.__importDefault) || function (mod) {
51
+ return (mod && mod.__esModule) ? mod : { "default": mod };
52
+ };
53
+ Object.defineProperty(exports, "__esModule", { value: true });
54
+ exports.PhoneNetworkRestore = exports.LEFTOVER_MAX_AGE_MS = void 0;
55
+ const typedi_1 = require("typedi");
56
+ const logger_1 = __importDefault(require("../../logger"));
57
+ const PortAllocator_1 = require("../PortAllocator");
58
+ const AndroidProxyAdapter_1 = require("../interceptor/AndroidProxyAdapter");
59
+ const adbForPhone_1 = require("./adbForPhone");
60
+ const AndroidRadios_1 = require("./AndroidRadios");
61
+ const PhoneNetworkLedger_1 = require("./PhoneNetworkLedger");
62
+ const phoneNetworkLock_1 = require("./phoneNetworkLock");
63
+ const xenonProxy_1 = require("./xenonProxy");
64
+ /** A record that still can't be undone after this long is dropped: the phone is gone. */
65
+ exports.LEFTOVER_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
66
+ /**
67
+ * Puts a phone's network back after a session that changed it: a network
68
+ * profile (`Offline` turns Wi-Fi and mobile data off) or the network
69
+ * interceptor (the phone's global HTTP proxy). Both change the whole phone,
70
+ * not one app, so a phone left that way has no network for the next test.
71
+ *
72
+ * Every way a session ends on this server calls restoreSession, before the
73
+ * phone is released so the next session can't have its own changes undone:
74
+ * the test's delete (SessionLifecycleService.deleteSession), Appium's own end
75
+ * of a session (its new-command timeout, a crashed driver: the plugin's
76
+ * onUnexpectedShutdown), Xenon's idle release (releaseBlockedDevices), a
77
+ * stale heartbeat (OrphanSweeper) and a graceful shutdown. It is keyed by the
78
+ * session id alone, so it works for sessions Xenon keeps no other record of
79
+ * (SESSION_MANAGER holds a local session only with the dashboard or video on).
80
+ *
81
+ * Once per session: a second ending finds the first one's work in progress
82
+ * and waits for it, and a later one finds nothing to do.
83
+ *
84
+ * What a crash forgets, PhoneNetworkLedger remembers. restoreLeftovers undoes
85
+ * it at this server's next start (cleanUpAtBoot) and before the phone's next
86
+ * session here (a phone that was unplugged when its session ended).
87
+ */
88
+ let PhoneNetworkRestore = class PhoneNetworkRestore {
89
+ constructor() {
90
+ this.logger = logger_1.default.scope('PhoneNetwork');
91
+ this.inflight = new Map();
92
+ this.radios = new AndroidRadios_1.AndroidRadios(adbForPhone_1.adbForPhone);
93
+ this.proxies = new AndroidProxyAdapter_1.AndroidProxyAdapter(adbForPhone_1.adbForPhone);
94
+ }
95
+ /** Undoes what the session changed on its phone's network, once. Never throws. */
96
+ restoreSession(sessionId, why) {
97
+ if (!sessionId)
98
+ return Promise.resolve();
99
+ const running = this.inflight.get(sessionId);
100
+ if (running)
101
+ return running;
102
+ const work = this.undoSession(sessionId, why).finally(() => this.inflight.delete(sessionId));
103
+ this.inflight.set(sessionId, work);
104
+ return work;
105
+ }
106
+ /** Every session this server still has network changes for (shutdown). Never throws. */
107
+ restoreAll(why) {
108
+ return __awaiter(this, void 0, void 0, function* () {
109
+ const { net, icp } = yield this.services();
110
+ const ids = new Set([...net.sessionIds(), ...icp.sessionIds(), ...this.inflight.keys()]);
111
+ yield Promise.all([...ids].map((id) => this.restoreSession(id, why)));
112
+ });
113
+ }
114
+ /**
115
+ * Undoes what the ledger says ended sessions left on their phones: one
116
+ * phone's (`udid`) or every phone's. Skips sessions this server still runs.
117
+ * A record that can't be undone now (the phone isn't connected) is kept for
118
+ * next time, for a week. Returns how many records were fully undone.
119
+ * Never throws.
120
+ */
121
+ restoreLeftovers() {
122
+ return __awaiter(this, arguments, void 0, function* (opts = {}) {
123
+ var _a;
124
+ let done = 0;
125
+ try {
126
+ const ledger = typedi_1.Container.get(PhoneNetworkLedger_1.PhoneNetworkLedger);
127
+ const entries = (yield ledger.list()).filter((e) => !opts.udid || e.udid === opts.udid);
128
+ for (const entry of entries) {
129
+ if (yield this.isLive(entry.sessionId))
130
+ continue;
131
+ if (yield this.undoLeftover(entry.sessionId, entry.udid))
132
+ done++;
133
+ }
134
+ }
135
+ catch (err) {
136
+ this.logger.warn(`Could not put back what earlier sessions left: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
137
+ }
138
+ return done;
139
+ });
140
+ }
141
+ /**
142
+ * At this server's start: first what the ledger holds, then each of this
143
+ * server's Android phones (`udids`) whose proxy is a capture an earlier run
144
+ * left (this machine, one of this server's capture ports, nothing answering
145
+ * there; such a phone has no network). That second check also catches a
146
+ * crash from before the ledger existed. A proxy that points anywhere else,
147
+ * or at a capture that is running, is left alone. Never throws.
148
+ */
149
+ cleanUpAtBoot(udids) {
150
+ return __awaiter(this, void 0, void 0, function* () {
151
+ var _a;
152
+ yield this.restoreLeftovers({});
153
+ let range;
154
+ try {
155
+ range = typedi_1.Container.get(PortAllocator_1.PortAllocator).rangeOf('proxy');
156
+ }
157
+ catch (err) {
158
+ this.logger.warn(`Could not check the phones' proxies: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
159
+ return;
160
+ }
161
+ for (const udid of udids)
162
+ yield this.clearDeadCaptureProxy(udid, range);
163
+ });
164
+ }
165
+ undoSession(sessionId, why) {
166
+ return __awaiter(this, void 0, void 0, function* () {
167
+ var _a, _b;
168
+ let services;
169
+ try {
170
+ services = yield this.services();
171
+ }
172
+ catch (err) {
173
+ this.logger.warn(`Could not put back the network of session ${sessionId}: ${err === null || err === void 0 ? void 0 : err.message}`);
174
+ return;
175
+ }
176
+ const { net, icp } = services;
177
+ const profile = net.has(sessionId);
178
+ const capture = icp.isActive(sessionId);
179
+ if (!profile && !capture)
180
+ return;
181
+ this.logger.info(`Putting back the phone network of session ${sessionId} (${why})`);
182
+ if (profile) {
183
+ try {
184
+ yield net.reset(sessionId);
185
+ }
186
+ catch (err) {
187
+ this.logger.warn(`Network profile reset failed for ${sessionId}: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
188
+ }
189
+ }
190
+ if (capture) {
191
+ try {
192
+ yield icp.stop(sessionId);
193
+ }
194
+ catch (err) {
195
+ this.logger.warn(`Interceptor stop failed for ${sessionId}: ${(_b = err === null || err === void 0 ? void 0 : err.message) !== null && _b !== void 0 ? _b : err}`);
196
+ }
197
+ }
198
+ });
199
+ }
200
+ isLive(sessionId) {
201
+ return __awaiter(this, void 0, void 0, function* () {
202
+ if (this.inflight.has(sessionId))
203
+ return true;
204
+ const { net, icp } = yield this.services();
205
+ return net.has(sessionId) || icp.isActive(sessionId);
206
+ });
207
+ }
208
+ /** One record, under the phone's lock, read again there: another clean-up may have done it. */
209
+ undoLeftover(sessionId, udid) {
210
+ return __awaiter(this, void 0, void 0, function* () {
211
+ const ledger = typedi_1.Container.get(PhoneNetworkLedger_1.PhoneNetworkLedger);
212
+ return yield (0, phoneNetworkLock_1.withPhoneNetworkLock)(udid, () => __awaiter(this, void 0, void 0, function* () {
213
+ const change = yield ledger.get(sessionId);
214
+ if (!change)
215
+ return false;
216
+ let undone = true;
217
+ if (change.offline && !(yield this.undoOffline(change, change.offline)))
218
+ undone = false;
219
+ if (change.proxy && !(yield this.undoProxy(change, change.proxy)))
220
+ undone = false;
221
+ if (!undone && Date.now() - change.at > exports.LEFTOVER_MAX_AGE_MS) {
222
+ yield ledger.forget(sessionId);
223
+ this.logger.warn(`Gave up putting back the network of ${udid} (session ${sessionId}): it has not ` +
224
+ 'been reachable for a week.');
225
+ }
226
+ return undone;
227
+ }));
228
+ });
229
+ }
230
+ undoOffline(change, prior) {
231
+ return __awaiter(this, void 0, void 0, function* () {
232
+ var _a;
233
+ const { udid, sessionId } = change;
234
+ const which = AndroidRadios_1.AndroidRadios.toRestore(prior);
235
+ try {
236
+ yield this.radios.turnOn(udid, which);
237
+ yield typedi_1.Container.get(PhoneNetworkLedger_1.PhoneNetworkLedger).settle(sessionId, 'offline');
238
+ const what = [which.wifi && 'Wi-Fi', which.data && 'mobile data']
239
+ .filter(Boolean)
240
+ .join(' and ');
241
+ if (what) {
242
+ this.logger.warn(`Turned ${what} back on on ${udid}: session ${sessionId} took the phone offline and ` +
243
+ 'ended without putting it back.');
244
+ }
245
+ return true;
246
+ }
247
+ catch (err) {
248
+ this.logger.warn(`Could not turn the network back on on ${udid} (session ${sessionId}): ` +
249
+ `${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}. Trying again at its next session and at the next start.`);
250
+ return false;
251
+ }
252
+ });
253
+ }
254
+ undoProxy(change, proxy) {
255
+ return __awaiter(this, void 0, void 0, function* () {
256
+ var _a, _b;
257
+ const { udid, sessionId } = change;
258
+ try {
259
+ const now = (yield this.proxies.readProxy(udid)).trim();
260
+ if (now === proxy.set) {
261
+ yield this.proxies.restoreProxy(udid, proxy.previous);
262
+ if (proxy.reversePort)
263
+ yield this.removeReverse(udid, proxy.reversePort);
264
+ this.logger.warn(`Put back the proxy of ${udid} (${(_a = proxy.previous) !== null && _a !== void 0 ? _a : 'none'}): session ${sessionId} ` +
265
+ `pointed it at ${proxy.set} and ended without putting it back.`);
266
+ }
267
+ else {
268
+ this.logger.info(`Left the proxy of ${udid} as it is (${now || 'none'}): it changed after session ` +
269
+ `${sessionId} set ${proxy.set}.`);
270
+ }
271
+ yield typedi_1.Container.get(PhoneNetworkLedger_1.PhoneNetworkLedger).settle(sessionId, 'proxy');
272
+ return true;
273
+ }
274
+ catch (err) {
275
+ this.logger.warn(`Could not put back the proxy of ${udid} (session ${sessionId}): ${(_b = err === null || err === void 0 ? void 0 : err.message) !== null && _b !== void 0 ? _b : err}. ` +
276
+ 'Trying again at its next session and at the next start.');
277
+ return false;
278
+ }
279
+ });
280
+ }
281
+ clearDeadCaptureProxy(udid, range) {
282
+ return __awaiter(this, void 0, void 0, function* () {
283
+ var _a;
284
+ try {
285
+ yield (0, phoneNetworkLock_1.withPhoneNetworkLock)(udid, () => __awaiter(this, void 0, void 0, function* () {
286
+ const now = yield this.proxies.readProxy(udid);
287
+ const proxy = (0, xenonProxy_1.parseProxySetting)(now);
288
+ if (!proxy || !(yield (0, xenonProxy_1.isDeadCaptureProxy)(now, range)))
289
+ return;
290
+ yield this.proxies.clearProxy(udid);
291
+ if (proxy.host === '127.0.0.1' || proxy.host.toLowerCase() === 'localhost') {
292
+ yield this.removeReverse(udid, proxy.port);
293
+ }
294
+ this.logger.warn(`Cleared the proxy of ${udid} (${now.trim()}): it pointed at this machine's network ` +
295
+ `capture port ${proxy.port}, where nothing answers, so the phone had no network.`);
296
+ }));
297
+ }
298
+ catch (err) {
299
+ this.logger.debug(`Could not check the proxy of ${udid}: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
300
+ }
301
+ });
302
+ }
303
+ removeReverse(udid, port) {
304
+ return __awaiter(this, void 0, void 0, function* () {
305
+ try {
306
+ yield this.proxies.removeReverse(udid, port);
307
+ }
308
+ catch (_a) {
309
+ // Gone with the adb server or the connection: nothing to remove.
310
+ }
311
+ });
312
+ }
313
+ /** Imported lazily: InterceptorService pulls in the socket server. */
314
+ services() {
315
+ return __awaiter(this, void 0, void 0, function* () {
316
+ const [{ NetworkConditioningService }, { InterceptorService }] = yield Promise.all([
317
+ Promise.resolve().then(() => __importStar(require('../NetworkConditioningService'))),
318
+ Promise.resolve().then(() => __importStar(require('../InterceptorService'))),
319
+ ]);
320
+ return {
321
+ net: typedi_1.Container.get(NetworkConditioningService),
322
+ icp: typedi_1.Container.get(InterceptorService),
323
+ };
324
+ });
325
+ }
326
+ };
327
+ exports.PhoneNetworkRestore = PhoneNetworkRestore;
328
+ exports.PhoneNetworkRestore = PhoneNetworkRestore = __decorate([
329
+ (0, typedi_1.Service)()
330
+ ], PhoneNetworkRestore);
@@ -0,0 +1,60 @@
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
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.adbForPhone = void 0;
46
+ const typedi_1 = require("typedi");
47
+ /**
48
+ * A phone's adb as the rest of Xenon reaches it: appium-adb's, which finds the
49
+ * binary through ANDROID_HOME / ANDROID_SDK_ROOT and runs it without a shell,
50
+ * and which talks to a remote adb server for a phone that has one. Never a
51
+ * bare `adb`: a server started without the SDK on its PATH (Xenon Control, a
52
+ * service) has none.
53
+ *
54
+ * Imported lazily: AndroidDeviceManager pulls in most of the device layer.
55
+ */
56
+ const adbForPhone = (udid) => __awaiter(void 0, void 0, void 0, function* () {
57
+ const { default: AndroidDeviceManager } = yield Promise.resolve().then(() => __importStar(require('../../device-managers/AndroidDeviceManager')));
58
+ return yield typedi_1.Container.get(AndroidDeviceManager).getAdbForDevice(udid);
59
+ });
60
+ exports.adbForPhone = adbForPhone;
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.withPhoneNetworkLock = withPhoneNetworkLock;
7
+ const async_lock_1 = __importDefault(require("async-lock"));
8
+ const lock = new async_lock_1.default({ maxPending: 10000 });
9
+ /**
10
+ * One change at a time to a phone's network settings (Wi-Fi, mobile data, the
11
+ * global proxy): a session's start reading and changing them, its end putting
12
+ * them back, and a clean-up of what an earlier run left. Without it, a
13
+ * clean-up that read a dead proxy could clear the one a new session had just
14
+ * set, or a session could read another's half-undone state as the phone's own.
15
+ *
16
+ * Not reentrant: never call it from inside itself for the same phone.
17
+ */
18
+ function withPhoneNetworkLock(udid, fn) {
19
+ return lock.acquire(`phone-network:${udid}`, fn);
20
+ }