@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.
- package/README.md +11 -5
- package/lib/package.json +1 -1
- package/lib/public/assets/{AnnotationOverlay-BXruClDk.js → AnnotationOverlay-B_VNtKBH.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-qTS4LPLC.js → ApiKeyGate-BwuaMjkX.js} +1 -1
- package/lib/public/assets/{BugReportButton-BoGhlI7F.js → BugReportButton-BAt0_pzq.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +2 -0
- package/lib/public/assets/{EmptyState-Cr17PdDv.js → EmptyState-BSjhk_IV.js} +1 -1
- package/lib/public/assets/{FieldGroup-Bvn08svv.js → FieldGroup-Cb5QZsE1.js} +1 -1
- package/lib/public/assets/{FilterMenu-DDSO8aDf.js → FilterMenu-BAaSlWbW.js} +1 -1
- package/lib/public/assets/{Menu-B-Qn2X3T.js → Menu-JGGYrSQQ.js} +1 -1
- package/lib/public/assets/{Modal-Cg9V5JYU.js → Modal-CFCp3n9d.js} +1 -1
- package/lib/public/assets/{RecordingPage-BTUs9VTW.js → RecordingPage-CbJj_EBT.js} +1 -1
- package/lib/public/assets/{RecordingsPage-DYcfxITK.js → RecordingsPage-BJeYnygR.js} +2 -2
- package/lib/public/assets/{SegmentedControl-Zy7suhT5.js → SegmentedControl-Bg42Atjy.js} +1 -1
- package/lib/public/assets/{SettingCard-CWYmycoe.js → SettingCard-Njz1pUIJ.js} +1 -1
- package/lib/public/assets/{Table-io9fwa98.js → Table-BcTm5xV2.js} +1 -1
- package/lib/public/assets/{activity-Bdf_uIWl.js → activity-DOVdsfMQ.js} +1 -1
- package/lib/public/assets/{ai-settings-Bce5SJob.js → ai-settings-CsuJroRf.js} +2 -2
- package/lib/public/assets/{api-keys-CPqtVpm3.js → api-keys-BKnNSTw9.js} +1 -1
- package/lib/public/assets/{apps-BgZan0H9.js → apps-D0SC6uw6.js} +2 -2
- package/lib/public/assets/{arrow-left-B27ckB31.js → arrow-left-MMbTmgoE.js} +1 -1
- package/lib/public/assets/{arrow-right-DOF8ITCz.js → arrow-right-BMfnGmxW.js} +1 -1
- package/lib/public/assets/{arrow-up-right-eFG5KTFS.js → arrow-up-right-DuAftMWB.js} +1 -1
- package/lib/public/assets/{auth-shell-DlC3_27j.js → auth-shell-nZC2JNDO.js} +2 -2
- package/lib/public/assets/{builds-page-BuXpPfKo.js → builds-page-hDCgTwkS.js} +1 -1
- package/lib/public/assets/{button-fIW8owMI.js → button-Dda4irn_.js} +1 -1
- package/lib/public/assets/{calendar-WNjmrF-a.js → calendar-BMr0Q62v.js} +1 -1
- package/lib/public/assets/{check-BdTh1Sq7.js → check-CZ9iw9-e.js} +1 -1
- package/lib/public/assets/{chevron-right-B4BKlsqk.js → chevron-right-BRYJJVC9.js} +1 -1
- package/lib/public/assets/{circle-check-zJU80_EF.js → circle-check-CqAJjQ5O.js} +1 -1
- package/lib/public/assets/{circle-x-5Uwquo0h.js → circle-x-DlDJtnVz.js} +1 -1
- package/lib/public/assets/{clock-BtUprDsZ.js → clock-CpuDKysU.js} +1 -1
- package/lib/public/assets/{copy-DGSbZI3u.js → copy-CAQydNFe.js} +1 -1
- package/lib/public/assets/device-explorer-CdO6_nrn.js +282 -0
- package/lib/public/assets/{download-WG6l7BgD.js → download-C9XG6lLv.js} +1 -1
- package/lib/public/assets/{forgot-password-DnudqFFo.js → forgot-password-CGnkRgE9.js} +1 -1
- package/lib/public/assets/h264Stream-VQBphYhg.js +26 -0
- package/lib/public/assets/{index-DI1fGIyj.js → index-BbKEXUFH.js} +5 -5
- package/lib/public/assets/{index-HDQVe1gi.js → index-DJGc1M2f.js} +1 -1
- package/lib/public/assets/{input-BjDRKCea.js → input-Cm0tmSoc.js} +1 -1
- package/lib/public/assets/{line-chart-C0zdgd0v.js → line-chart-D2DyJCml.js} +1 -1
- package/lib/public/assets/{list-checks-BdW5CjM8.js → list-checks-UWf1GY99.js} +1 -1
- package/lib/public/assets/{lock-Dy3L4I8E.js → lock-CCJv9m_I.js} +1 -1
- package/lib/public/assets/{login-BHbY9CLG.js → login-f8vOVW-U.js} +1 -1
- package/lib/public/assets/maintenance-settings-C1PrkuAH.js +6 -0
- package/lib/public/assets/{monitor-8xjkeHHi.js → monitor-B2jDgwm7.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CXdKMXQ8.js → mouse-pointer-2-DLI1RtzU.js} +1 -1
- package/lib/public/assets/{network-Cj8JOKON.js → network-ebT86qoi.js} +1 -1
- package/lib/public/assets/{overview-CTg7F0im.js → overview-C0aykVrd.js} +1 -1
- package/lib/public/assets/{page-header--aTyJXq6.js → page-header-BjzgWz-T.js} +1 -1
- package/lib/public/assets/{play-B-FodgHV.js → play-DLG9t8FU.js} +1 -1
- package/lib/public/assets/{plus-CsIk10Cq.js → plus-nBFSMu_X.js} +1 -1
- package/lib/public/assets/{profile-page-DkFKkMfl.js → profile-page-CSl0UYlv.js} +1 -1
- package/lib/public/assets/{reset-password-I2-zqLlI.js → reset-password-LeOuHqmB.js} +1 -1
- package/lib/public/assets/{runbook-page-DJTVCJX7.js → runbook-page-CMMeQml9.js} +1 -1
- package/lib/public/assets/{select-DvCSv25z.js → select-88jHcl-Z.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BLVfMJs_.js → selector-detail-redirect-BbXKJjZM.js} +1 -1
- package/lib/public/assets/{selector-health-page-oqadRokn.js → selector-health-page-B4rA1Xkv.js} +2 -2
- package/lib/public/assets/session-detail-page-COL_RZxw.js +4 -0
- package/lib/public/assets/settings-GF6ZMSQ6.js +6 -0
- package/lib/public/assets/{stat-tile-DV9Ix-4O.js → stat-tile-BV98e_Sv.js} +1 -1
- package/lib/public/assets/{tablet-6b_SEN2X.js → tablet-BiAsnFDj.js} +1 -1
- package/lib/public/assets/{teams-BjZyfuHh.js → teams-B4-jrr-1.js} +1 -1
- package/lib/public/assets/{trash-2-CTZpPSdd.js → trash-2-C5RSuX0e.js} +1 -1
- package/lib/public/assets/{upload-P3XzwQUb.js → upload-BG7_rMXK.js} +1 -1
- package/lib/public/assets/{use-builds-data-1cOCMf-z.js → use-builds-data-J-riQYyC.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DgF9w088.js → use-password-reset-mode-C8-eH-UG.js} +1 -1
- package/lib/public/assets/{users-DMF9Mk5H.js → users-CFba5TSy.js} +1 -1
- package/lib/public/assets/{users-CiNTKUiz.js → users-Cp0beJm3.js} +1 -1
- package/lib/public/assets/{video-off-CHp1KU_T.js → video-off-DDsEISjV.js} +1 -1
- package/lib/public/assets/webhook-settings-BGu32vSB.js +1 -0
- package/lib/public/assets/{zap-DRz0tDq5.js → zap-l4VaXzq8.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +19 -21
- package/lib/src/XenonCapabilityManager.js +22 -5
- package/lib/src/app/openapi/control.yaml +71 -20
- package/lib/src/app/openapi/grid.yaml +23 -5
- package/lib/src/app/openapi/platform.yaml +136 -28
- package/lib/src/app/routers/control.js +12 -2
- package/lib/src/app/routers/dashboard.js +29 -1
- package/lib/src/app/routers/grid.js +32 -10
- package/lib/src/app/routers/streamType.js +7 -2
- package/lib/src/app/routers/webhook.js +8 -4
- package/lib/src/config.js +50 -5
- package/lib/src/dashboard/commands.js +185 -141
- package/lib/src/dashboard/event-manager.js +39 -6
- package/lib/src/data-service/appleFamily.js +43 -0
- package/lib/src/data-service/db.js +11 -0
- package/lib/src/data-service/device-service.js +6 -1
- package/lib/src/data-service/device-store.js +62 -2
- package/lib/src/data-service/deviceClaims.js +37 -0
- package/lib/src/data-service/deviceFieldOwners.js +25 -11
- package/lib/src/data-service/deviceSettings.js +79 -0
- package/lib/src/data-service/prisma-store.js +150 -8
- package/lib/src/data-service/web-config-service.js +43 -92
- package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
- package/lib/src/device-managers/ios/WDAClient.js +44 -43
- package/lib/src/device-managers/shellCommands.js +96 -0
- package/lib/src/device-utils.js +96 -40
- package/lib/src/generated/client/edge.js +17 -3
- package/lib/src/generated/client/index-browser.js +14 -0
- package/lib/src/generated/client/index.d.ts +2054 -379
- package/lib/src/generated/client/index.js +17 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +35 -7
- package/lib/src/generated/client/wasm.js +14 -0
- package/lib/src/interceptors/CommandInterceptor.js +29 -8
- package/lib/src/interceptors/xenonScripts.js +55 -0
- package/lib/src/interfaces/IPluginArgs.js +1 -2
- package/lib/src/plugin.js +15 -1
- package/lib/src/prisma.js +1 -1
- package/lib/src/scripts/database-check.js +124 -0
- package/lib/src/services/AIService.js +99 -0
- package/lib/src/services/CleanupService.js +6 -1
- package/lib/src/services/InterceptorService.js +70 -13
- package/lib/src/services/NetworkConditioningService.js +85 -88
- package/lib/src/services/NotificationService.js +36 -29
- package/lib/src/services/OrphanSweeper.js +5 -0
- package/lib/src/services/PortAllocator.js +5 -0
- package/lib/src/services/ServerManager.js +94 -8
- package/lib/src/services/SessionLifecycleService.js +47 -49
- package/lib/src/services/ShutdownCoordinator.js +33 -0
- package/lib/src/services/TeamService.js +20 -3
- package/lib/src/services/healing/HealingOrchestrator.js +32 -1
- package/lib/src/services/interceptor/AndroidProxyAdapter.js +18 -0
- package/lib/src/services/interceptor/interceptorOptions.js +44 -0
- package/lib/src/services/network/AndroidRadios.js +71 -0
- package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
- package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
- package/lib/src/services/network/adbForPhone.js +60 -0
- package/lib/src/services/network/phoneNetworkLock.js +20 -0
- package/lib/src/services/network/xenonProxy.js +107 -0
- package/lib/src/services/omni-vision/OmniVisionService.js +53 -43
- package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
- package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
- package/lib/src/services/omni-vision/screenScale.js +68 -0
- package/lib/src/services/recording/concurrency-gate.js +12 -3
- package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
- package/lib/src/services/settings/labSettings.js +149 -0
- package/lib/src/services/webhookEvents.js +173 -0
- package/lib/test/helpers/fake-adb.js +58 -0
- package/lib/test/helpers/fake-device-table.js +63 -24
- package/lib/test/helpers/scratch-database.js +1 -0
- package/lib/test/unit/TeamService.test.js +3 -0
- package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
- package/lib/test/unit/apple-family.spec.js +36 -0
- package/lib/test/unit/cleanup-settings-db.spec.js +103 -0
- package/lib/test/unit/cleanup-settings.spec.js +244 -0
- package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
- package/lib/test/unit/dashboard-commands.spec.js +249 -0
- package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
- package/lib/test/unit/database-check.spec.js +108 -0
- package/lib/test/unit/default-plugin-args.spec.js +33 -0
- package/lib/test/unit/device-offline-webhook.spec.js +81 -0
- package/lib/test/unit/device-setting-columns.spec.js +66 -0
- package/lib/test/unit/device-settings-loki.spec.js +87 -0
- package/lib/test/unit/device-settings-survive.spec.js +555 -0
- package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
- package/lib/test/unit/emulator-boot.spec.js +91 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +48 -0
- package/lib/test/unit/hub-node-same-host.spec.js +5 -0
- package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
- package/lib/test/unit/json-logging-option.spec.js +74 -0
- package/lib/test/unit/lab-settings.spec.js +218 -0
- package/lib/test/unit/max-sessions.spec.js +240 -0
- package/lib/test/unit/ocr-text-match.spec.js +44 -0
- package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
- package/lib/test/unit/omni-vision-ios-points.spec.js +161 -0
- package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
- package/lib/test/unit/omni-vision.spec.js +1 -1
- package/lib/test/unit/partial-config-file.spec.js +14 -0
- package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
- package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
- package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
- package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
- package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
- package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
- package/lib/test/unit/phone-network/session-start.spec.js +110 -0
- package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
- package/lib/test/unit/plugin.spec.js +3 -3
- package/lib/test/unit/recording-options.spec.js +181 -0
- package/lib/test/unit/self-healing-boot.spec.js +75 -0
- package/lib/test/unit/self-healing-setting.spec.js +272 -0
- package/lib/test/unit/session-failed-webhook.spec.js +170 -0
- package/lib/test/unit/shell-commands.spec.js +133 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
- package/lib/test/unit/stream-start-pipelines.spec.js +210 -0
- package/lib/test/unit/stream-status-type.spec.js +4 -0
- package/lib/test/unit/vision-assertion.spec.js +118 -0
- package/lib/test/unit/wda-client-shell.spec.js +133 -0
- package/lib/test/unit/web-config-service.spec.js +79 -0
- package/lib/test/unit/webhook-delivery.spec.js +24 -0
- package/lib/test/unit/webhook-payloads.spec.js +273 -0
- package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
- package/package.json +1 -1
- package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
- package/prisma/schema.prisma +35 -7
- package/schema.json +19 -21
- package/scripts/generate-types-from-schema.js +1 -2
- package/scripts/lib/always-present-plugin-args.js +4 -1
- package/lib/public/assets/DeviceMosaicView-8J0oYqMM.js +0 -2
- package/lib/public/assets/device-explorer-C-KnhMHI.js +0 -279
- package/lib/public/assets/maintenance-settings-zDVlOMVK.js +0 -6
- package/lib/public/assets/recording-group-store-WVM3hoXn.js +0 -26
- package/lib/public/assets/session-detail-page-CGn55Pvl.js +0 -4
- package/lib/public/assets/settings-MsVU2ukE.js +0 -6
- 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()
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|