@xenon-device-management/xenon 2.13.1 → 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-Cvpkk4r9.js → AnnotationOverlay-B_VNtKBH.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-Bhn5ZtVM.js → ApiKeyGate-BwuaMjkX.js} +1 -1
- package/lib/public/assets/{BugReportButton-BA6aEjTC.js → BugReportButton-BAt0_pzq.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +2 -0
- package/lib/public/assets/{EmptyState-CgzbuUvO.js → EmptyState-BSjhk_IV.js} +1 -1
- package/lib/public/assets/{FieldGroup-BHc_5qNc.js → FieldGroup-Cb5QZsE1.js} +1 -1
- package/lib/public/assets/{FilterMenu-D7OMGuRc.js → FilterMenu-BAaSlWbW.js} +1 -1
- package/lib/public/assets/{Menu-C173JJ0l.js → Menu-JGGYrSQQ.js} +1 -1
- package/lib/public/assets/{Modal-ChvVUI3B.js → Modal-CFCp3n9d.js} +1 -1
- package/lib/public/assets/{RecordingPage-Dhw4d_O6.js → RecordingPage-CbJj_EBT.js} +1 -1
- package/lib/public/assets/{RecordingsPage-z97ftGK-.js → RecordingsPage-BJeYnygR.js} +2 -2
- package/lib/public/assets/{SegmentedControl-DbXhBGZC.js → SegmentedControl-Bg42Atjy.js} +1 -1
- package/lib/public/assets/{SettingCard-C908GgiI.js → SettingCard-Njz1pUIJ.js} +1 -1
- package/lib/public/assets/{Table-B1DB_ZN-.js → Table-BcTm5xV2.js} +1 -1
- package/lib/public/assets/{activity-Css0xNvp.js → activity-DOVdsfMQ.js} +1 -1
- package/lib/public/assets/{ai-settings-BMQK3-yN.js → ai-settings-CsuJroRf.js} +2 -2
- package/lib/public/assets/{api-keys-CbJ42eDQ.js → api-keys-BKnNSTw9.js} +1 -1
- package/lib/public/assets/{apps-DW1pw2re.js → apps-D0SC6uw6.js} +2 -2
- package/lib/public/assets/{arrow-left-Dpog9L0F.js → arrow-left-MMbTmgoE.js} +1 -1
- package/lib/public/assets/{arrow-right-CsBIlSIw.js → arrow-right-BMfnGmxW.js} +1 -1
- package/lib/public/assets/{arrow-up-right-z1tRQE4R.js → arrow-up-right-DuAftMWB.js} +1 -1
- package/lib/public/assets/{auth-shell-mXiX5vNk.js → auth-shell-nZC2JNDO.js} +2 -2
- package/lib/public/assets/{builds-page-BcFn23bE.js → builds-page-hDCgTwkS.js} +1 -1
- package/lib/public/assets/{button-Cq_zHETv.js → button-Dda4irn_.js} +1 -1
- package/lib/public/assets/{calendar-CKxABX9q.js → calendar-BMr0Q62v.js} +1 -1
- package/lib/public/assets/{check-C7xu-hGU.js → check-CZ9iw9-e.js} +1 -1
- package/lib/public/assets/{chevron-right-BiqfdCGA.js → chevron-right-BRYJJVC9.js} +1 -1
- package/lib/public/assets/{circle-check-CyBjjPxR.js → circle-check-CqAJjQ5O.js} +1 -1
- package/lib/public/assets/{circle-x-6gMRcx6z.js → circle-x-DlDJtnVz.js} +1 -1
- package/lib/public/assets/{clock-U1rDTT1h.js → clock-CpuDKysU.js} +1 -1
- package/lib/public/assets/{copy-BIM0ZX0L.js → copy-CAQydNFe.js} +1 -1
- package/lib/public/assets/device-explorer-CdO6_nrn.js +282 -0
- package/lib/public/assets/{download-DDBRXnAZ.js → download-C9XG6lLv.js} +1 -1
- package/lib/public/assets/{forgot-password-Dpg7yxM8.js → forgot-password-CGnkRgE9.js} +1 -1
- package/lib/public/assets/h264Stream-VQBphYhg.js +26 -0
- package/lib/public/assets/{index-BGBqohfQ.js → index-BbKEXUFH.js} +5 -5
- package/lib/public/assets/{index-BTYMAqNW.css → index-Bxoxt9sV.css} +1 -1
- package/lib/public/assets/{index-BQ2luT7L.js → index-DJGc1M2f.js} +1 -1
- package/lib/public/assets/{input-BvB1cjHE.js → input-Cm0tmSoc.js} +1 -1
- package/lib/public/assets/{line-chart-m78VggnW.js → line-chart-D2DyJCml.js} +1 -1
- package/lib/public/assets/{list-checks-cY7o1QTL.js → list-checks-UWf1GY99.js} +1 -1
- package/lib/public/assets/{lock-loz8txQm.js → lock-CCJv9m_I.js} +1 -1
- package/lib/public/assets/{login-BYOvz4lx.js → login-f8vOVW-U.js} +1 -1
- package/lib/public/assets/maintenance-settings-C1PrkuAH.js +6 -0
- package/lib/public/assets/{monitor-C4HPHb6s.js → monitor-B2jDgwm7.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-BkdKtCzi.js → mouse-pointer-2-DLI1RtzU.js} +1 -1
- package/lib/public/assets/{network-C5e0MIWo.js → network-ebT86qoi.js} +1 -1
- package/lib/public/assets/{overview-tlu8ijGd.js → overview-C0aykVrd.js} +1 -1
- package/lib/public/assets/{page-header-FXPlo6az.js → page-header-BjzgWz-T.js} +1 -1
- package/lib/public/assets/{play-CWdk0_Sy.js → play-DLG9t8FU.js} +1 -1
- package/lib/public/assets/{plus-B2xCnntV.js → plus-nBFSMu_X.js} +1 -1
- package/lib/public/assets/{profile-page-D080qzr4.js → profile-page-CSl0UYlv.js} +1 -1
- package/lib/public/assets/{reset-password-D3u4Mbvn.js → reset-password-LeOuHqmB.js} +1 -1
- package/lib/public/assets/{runbook-page-hPRZMy7s.js → runbook-page-CMMeQml9.js} +1 -1
- package/lib/public/assets/{select-12RThAXc.js → select-88jHcl-Z.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-uQwvV7EQ.js → selector-detail-redirect-BbXKJjZM.js} +1 -1
- package/lib/public/assets/{selector-health-page-CThoxDFO.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-B02WEte8.js → stat-tile-BV98e_Sv.js} +1 -1
- package/lib/public/assets/{tablet-Dj30VGZl.js → tablet-BiAsnFDj.js} +1 -1
- package/lib/public/assets/{teams-uL4fjTDR.js → teams-B4-jrr-1.js} +1 -1
- package/lib/public/assets/{trash-2-B54e-TfY.js → trash-2-C5RSuX0e.js} +1 -1
- package/lib/public/assets/{upload-B3k8WCSP.js → upload-BG7_rMXK.js} +1 -1
- package/lib/public/assets/{use-builds-data-BFn90NH3.js → use-builds-data-J-riQYyC.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DL2TmWZ0.js → use-password-reset-mode-C8-eH-UG.js} +1 -1
- package/lib/public/assets/{users-CqBuxMlX.js → users-CFba5TSy.js} +1 -1
- package/lib/public/assets/{users-DjQSucKb.js → users-Cp0beJm3.js} +1 -1
- package/lib/public/assets/{video-off-T0rlt0GA.js → video-off-DDsEISjV.js} +1 -1
- package/lib/public/assets/webhook-settings-BGu32vSB.js +1 -0
- package/lib/public/assets/{zap-gBHqutZV.js → zap-l4VaXzq8.js} +1 -1
- package/lib/public/index.html +2 -2
- package/lib/schema.json +19 -46
- 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 +30 -5
- package/lib/src/app/routers/dashboard.js +29 -1
- package/lib/src/app/routers/grid.js +32 -10
- package/lib/src/app/routers/streamStartConflict.js +3 -0
- 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 +46 -1
- package/lib/src/data-service/device-store.js +66 -3
- 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 +152 -9
- 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 +143 -37
- 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/middleware/deviceAccessGuard.js +63 -9
- 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 +97 -8
- package/lib/src/services/SessionLifecycleService.js +63 -53
- package/lib/src/services/ShutdownCoordinator.js +33 -0
- package/lib/src/services/TeamService.js +20 -3
- package/lib/src/services/device-access/SessionOwnerResolver.js +26 -0
- package/lib/src/services/device-access/deviceAccessPolicy.js +38 -6
- package/lib/src/services/device-access/leaseHold.js +26 -0
- package/lib/src/services/device-access/ticketActorAccess.js +6 -0
- 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/lease/activeLeases.js +37 -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/busy-precheck.js +24 -3
- 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-access-lease.spec.js +270 -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/lease/allocate-lease-branch.spec.js +22 -0
- package/lib/test/unit/lease/session-lease-proof.spec.js +59 -0
- package/lib/test/unit/lease-held-phone.spec.js +196 -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 +114 -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/schema-required-args.spec.js +21 -16
- 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 -46
- package/scripts/generate-types-from-schema.js +7 -2
- package/scripts/lib/always-present-plugin-args.js +42 -0
- package/lib/public/assets/DeviceMosaicView-ChhHnpFQ.js +0 -2
- package/lib/public/assets/device-explorer-DSpAA_t9.js +0 -279
- package/lib/public/assets/maintenance-settings-BxbA7SkB.js +0 -6
- package/lib/public/assets/recording-group-store-B75435xR.js +0 -26
- package/lib/public/assets/session-detail-page-Bc02YjMm.js +0 -4
- package/lib/public/assets/settings-C1VIluuT.js +0 -6
- package/lib/public/assets/webhook-settings-DAHgnJyZ.js +0 -1
package/lib/src/device-utils.js
CHANGED
|
@@ -57,7 +57,7 @@ exports.initializeStorage = initializeStorage;
|
|
|
57
57
|
exports.getUtilizationTime = getUtilizationTime;
|
|
58
58
|
exports.setUtilizationTime = setUtilizationTime;
|
|
59
59
|
exports.getDeviceFiltersFromCapability = getDeviceFiltersFromCapability;
|
|
60
|
-
exports.
|
|
60
|
+
exports.getRunningSessionsCount = getRunningSessionsCount;
|
|
61
61
|
exports.updateDeviceList = updateDeviceList;
|
|
62
62
|
exports.refreshSimulatorState = refreshSimulatorState;
|
|
63
63
|
exports.setupCronCheckStaleDevices = setupCronCheckStaleDevices;
|
|
@@ -72,6 +72,7 @@ exports.cleanPendingSessions = cleanPendingSessions;
|
|
|
72
72
|
exports.setupCronCleanPendingSessions = setupCronCleanPendingSessions;
|
|
73
73
|
exports.setupCronCleanExpiredReservations = setupCronCleanExpiredReservations;
|
|
74
74
|
exports.setupCronCleanupBuilds = setupCronCleanupBuilds;
|
|
75
|
+
exports.rescheduleCleanupBuilds = rescheduleCleanupBuilds;
|
|
75
76
|
exports.setupCronReconcileDevices = setupCronReconcileDevices;
|
|
76
77
|
exports.setupCronSweepOrphanSessions = setupCronSweepOrphanSessions;
|
|
77
78
|
exports.setupCronSelectorVerification = setupCronSelectorVerification;
|
|
@@ -80,6 +81,7 @@ exports.stopAllTimers = stopAllTimers;
|
|
|
80
81
|
const helpers_1 = require("./helpers");
|
|
81
82
|
const XenonCapabilityManager_1 = require("./XenonCapabilityManager");
|
|
82
83
|
const async_wait_until_1 = __importDefault(require("async-wait-until"));
|
|
84
|
+
const async_lock_1 = __importDefault(require("async-lock"));
|
|
83
85
|
const typedi_1 = require("typedi");
|
|
84
86
|
const device_managers_1 = require("./device-managers");
|
|
85
87
|
const device_service_1 = require("./data-service/device-service");
|
|
@@ -99,6 +101,7 @@ const config_1 = require("./config");
|
|
|
99
101
|
const device_store_1 = require("./data-service/device-store");
|
|
100
102
|
const leaseSessionCaps_1 = require("./services/lease/leaseSessionCaps");
|
|
101
103
|
const deviceVisibility_1 = require("./services/device-access/deviceVisibility");
|
|
104
|
+
const activeLeases_1 = require("./services/lease/activeLeases");
|
|
102
105
|
// Use a Proxy to ensure we're always using the latest store from the factory,
|
|
103
106
|
// which is critical for test isolation when the factory cache is cleared.
|
|
104
107
|
const pendingStore = new Proxy({}, {
|
|
@@ -151,6 +154,9 @@ function isDeviceConfigPathAbsolute(path) {
|
|
|
151
154
|
throw new Error(`Device Config Path ${path} should be absolute`);
|
|
152
155
|
}
|
|
153
156
|
}
|
|
157
|
+
// One at a time through "is there a free slot, and which phone": see allocateDeviceForSession.
|
|
158
|
+
const ALLOCATION_LOCK = new async_lock_1.default();
|
|
159
|
+
const ALLOCATION_LOCK_KEY = 'allocate';
|
|
154
160
|
// What a caller that passes no proof has: nothing. A lease then needs its token.
|
|
155
161
|
const NO_LEASE_PROOF = {
|
|
156
162
|
canOverride: false,
|
|
@@ -182,6 +188,14 @@ function allocateDeviceForSession(capability_1, deviceTimeOutMs_1, deviceQueryIn
|
|
|
182
188
|
// text or its stack tells them apart.
|
|
183
189
|
if (!leasedDevice)
|
|
184
190
|
throw new Error((0, leaseSessionCaps_1.leaseRefusalMessage)(leaseIdCap));
|
|
191
|
+
// As for an allocated phone below: the idle sweeper reads this session's
|
|
192
|
+
// own timeout from the row, and its idle time from now, not from when the
|
|
193
|
+
// lease was taken. Without it a leased session was ended after the
|
|
194
|
+
// server-wide newCommandTimeoutSec, whatever the session asked for.
|
|
195
|
+
yield (0, device_service_1.updatedAllocatedDevice)(leasedDevice, {
|
|
196
|
+
newCommandTimeout: sessionNewCommandTimeout(firstMatch, pluginArgs),
|
|
197
|
+
lastCmdExecutedAt: Date.now(),
|
|
198
|
+
});
|
|
185
199
|
return leasedDevice;
|
|
186
200
|
}
|
|
187
201
|
const filters = getDeviceFiltersFromCapability(firstMatch, pluginArgs);
|
|
@@ -195,29 +209,54 @@ function allocateDeviceForSession(capability_1, deviceTimeOutMs_1, deviceQueryIn
|
|
|
195
209
|
const intervalBetweenAttempts = firstMatch[customCapability.deviceQueryInterval] || deviceQueryIntervalMs;
|
|
196
210
|
let device = null;
|
|
197
211
|
const store = device_store_1.DeviceStoreFactory.getStore();
|
|
212
|
+
// Set once the wait has given up. waitUntil rejects on its timeout but
|
|
213
|
+
// doesn't stop an attempt already running, which could then claim a phone
|
|
214
|
+
// for a create that has failed: a pending claim nobody would ever use.
|
|
215
|
+
let abandoned = false;
|
|
198
216
|
try {
|
|
199
217
|
yield (0, async_wait_until_1.default)(() => __awaiter(this, void 0, void 0, function* () {
|
|
200
|
-
|
|
201
|
-
const busyDevicesCount = yield getBusyDevicesCount();
|
|
202
|
-
if (maxSessions !== undefined && busyDevicesCount === maxSessions) {
|
|
203
|
-
logger_1.default.info(`Waiting for session available, already at max session count of: ${maxSessions}`);
|
|
218
|
+
if (abandoned)
|
|
204
219
|
return false;
|
|
205
|
-
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
220
|
+
// The limit check and the claim are one step. Requests that each read
|
|
221
|
+
// "one slot left" before any of them claimed would all take a phone and
|
|
222
|
+
// run past the limit, and then no later check would see it fixed.
|
|
223
|
+
return ALLOCATION_LOCK.acquire(ALLOCATION_LOCK_KEY, () => __awaiter(this, void 0, void 0, function* () {
|
|
224
|
+
if (abandoned)
|
|
225
|
+
return false;
|
|
226
|
+
const maxSessions = (0, deviceClaims_1.sessionCap)(getDeviceManager().getMaxSessionCount());
|
|
227
|
+
if (maxSessions !== undefined) {
|
|
228
|
+
const running = yield getRunningSessionsCount();
|
|
229
|
+
if (running >= maxSessions) {
|
|
230
|
+
logger_1.default.info(`Waiting for session available, already at max session count of: ${maxSessions}`);
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Get matching devices that are not reserved
|
|
235
|
+
const candidates = (yield (0, device_service_1.getDevices)(filters)).filter((d) => !(0, device_service_1.isDeviceReserved)(d));
|
|
236
|
+
if (candidates.length > 0) {
|
|
237
|
+
// Principal Intelligence: Multi-node consistent locking
|
|
238
|
+
// Atomically claim one of them: busy, with a pending claim for the
|
|
239
|
+
// session being created (deviceClaims.ts). Every candidate, not just
|
|
240
|
+
// the first: the lock skips a phone an SDK lease holds, and offering
|
|
241
|
+
// it alone stalled the create while other phones were free. Exact
|
|
242
|
+
// (udid, host) pairs: a udid repeats across hosts, and a reserved
|
|
243
|
+
// twin of a candidate must not be locked in its place.
|
|
244
|
+
const locked = yield store.findAndLockDevice(Object.assign(Object.assign({}, filters), { udid: [...new Set(candidates.map((d) => d.udid))] }), { claim: true, only: new Set(candidates.map((d) => `${d.udid}@${d.host}`)) });
|
|
245
|
+
if (locked && abandoned) {
|
|
246
|
+
yield (0, device_service_1.releasePendingClaim)(locked);
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
device = locked;
|
|
250
|
+
if (device !== null)
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
logger_1.default.info(`Waiting for free device. Filter: ${JSON.stringify(filters)}}`);
|
|
254
|
+
return false;
|
|
255
|
+
}));
|
|
218
256
|
}), { timeout, intervalBetweenAttempts });
|
|
219
257
|
}
|
|
220
258
|
catch (err) {
|
|
259
|
+
abandoned = true;
|
|
221
260
|
// figure out whether the device is simply busy or non-existent
|
|
222
261
|
const filterCopy = Object.assign({}, filters);
|
|
223
262
|
delete filterCopy.busy;
|
|
@@ -258,11 +297,9 @@ function allocateDeviceForSession(capability_1, deviceTimeOutMs_1, deviceQueryIn
|
|
|
258
297
|
// Since findAndLockDevice already sets busy: true, we don't need blockDevice(device.udid, device.host);
|
|
259
298
|
logger_1.default.info(`📱 Locked device ${lockedDevice.udid} at host ${lockedDevice.host} for new session`);
|
|
260
299
|
yield updateCapabilityForDevice(capability, lockedDevice);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
yield (0, device_service_1.updatedAllocatedDevice)(lockedDevice, { newCommandTimeout });
|
|
300
|
+
yield (0, device_service_1.updatedAllocatedDevice)(lockedDevice, {
|
|
301
|
+
newCommandTimeout: sessionNewCommandTimeout(firstMatch, pluginArgs),
|
|
302
|
+
});
|
|
266
303
|
return lockedDevice;
|
|
267
304
|
}
|
|
268
305
|
else {
|
|
@@ -271,6 +308,10 @@ function allocateDeviceForSession(capability_1, deviceTimeOutMs_1, deviceQueryIn
|
|
|
271
308
|
}
|
|
272
309
|
});
|
|
273
310
|
}
|
|
311
|
+
/** The session's own `appium:newCommandTimeout` (seconds), else the server's. */
|
|
312
|
+
function sessionNewCommandTimeout(firstMatch, pluginArgs) {
|
|
313
|
+
return firstMatch['appium:newCommandTimeout'] || pluginArgs.newCommandTimeoutSec;
|
|
314
|
+
}
|
|
274
315
|
/**
|
|
275
316
|
* The device a lease-bound session may use, or null when it may not: the lease
|
|
276
317
|
* is missing, inactive or expired, the session did not prove it holds it, or
|
|
@@ -419,12 +460,14 @@ function getDeviceFiltersFromCapability(capability, pluginArgs) {
|
|
|
419
460
|
if ((deviceType === null || deviceType === void 0 ? void 0 : deviceType.startsWith('real')) && pluginArgs.iosDeviceType == 'simulated') {
|
|
420
461
|
throw new Error('iosDeviceType value is set to "simulated" but app provided is not suitable for simulator device.');
|
|
421
462
|
}
|
|
422
|
-
|
|
463
|
+
// iPad wins when both are asked for. The device store applies it; it is not
|
|
464
|
+
// a name match (a real phone is named by its owner), see appleFamilyOf.
|
|
465
|
+
let appleFamily = undefined;
|
|
423
466
|
if (capability[customCapability.ipadOnly]) {
|
|
424
|
-
|
|
467
|
+
appleFamily = 'ipad';
|
|
425
468
|
}
|
|
426
469
|
else if (capability[customCapability.iphoneOnly]) {
|
|
427
|
-
|
|
470
|
+
appleFamily = 'iphone';
|
|
428
471
|
}
|
|
429
472
|
// Ensure udid is always an array of strings for the filter
|
|
430
473
|
let udidFilter = [];
|
|
@@ -442,7 +485,6 @@ function getDeviceFiltersFromCapability(capability, pluginArgs) {
|
|
|
442
485
|
platformVersion: capability['appium:platformVersion']
|
|
443
486
|
? capability['appium:platformVersion']
|
|
444
487
|
: undefined,
|
|
445
|
-
name,
|
|
446
488
|
deviceType,
|
|
447
489
|
udid: udidFilter,
|
|
448
490
|
busy: false,
|
|
@@ -454,8 +496,8 @@ function getDeviceFiltersFromCapability(capability, pluginArgs) {
|
|
|
454
496
|
? capability[customCapability.tags].split(',').map(lodash_1.default.trim)
|
|
455
497
|
: undefined,
|
|
456
498
|
};
|
|
457
|
-
if (
|
|
458
|
-
caps = Object.assign(Object.assign({}, caps), {
|
|
499
|
+
if (appleFamily !== undefined) {
|
|
500
|
+
caps = Object.assign(Object.assign({}, caps), { appleFamily });
|
|
459
501
|
}
|
|
460
502
|
return caps;
|
|
461
503
|
}
|
|
@@ -465,12 +507,16 @@ function getDeviceFiltersFromCapability(capability, pluginArgs) {
|
|
|
465
507
|
function getDeviceManager() {
|
|
466
508
|
return typedi_1.Container.get(device_managers_1.XenonManager);
|
|
467
509
|
}
|
|
468
|
-
|
|
510
|
+
/**
|
|
511
|
+
* How many phones run an Appium session, or are being given one: what
|
|
512
|
+
* `maxSessions` limits. A preview, a recording or an idle SDK lease keeps a
|
|
513
|
+
* phone busy without being a session, so counting busy phones would be wrong
|
|
514
|
+
* (see `countsTowardMaxSessions`). On a hub it covers its nodes' phones too.
|
|
515
|
+
*/
|
|
516
|
+
function getRunningSessionsCount() {
|
|
469
517
|
return __awaiter(this, void 0, void 0, function* () {
|
|
470
518
|
const allDevices = yield (0, device_service_1.getAllDevices)();
|
|
471
|
-
return allDevices.filter(
|
|
472
|
-
return device.busy;
|
|
473
|
-
}).length;
|
|
519
|
+
return allDevices.filter(deviceClaims_1.countsTowardMaxSessions).length;
|
|
474
520
|
});
|
|
475
521
|
}
|
|
476
522
|
function updateDeviceList(local, hubArgument, tlsRejectUnauthorized) {
|
|
@@ -602,8 +648,25 @@ function unblockCandidateDevices() {
|
|
|
602
648
|
}
|
|
603
649
|
function releaseBlockedDevices(newCommandTimeout) {
|
|
604
650
|
return __awaiter(this, void 0, void 0, function* () {
|
|
605
|
-
var _a;
|
|
651
|
+
var _a, _b;
|
|
606
652
|
const busyDevices = yield unblockCandidateDevices();
|
|
653
|
+
// A phone an SDK lease holds is the lease's to time out (its heartbeats and
|
|
654
|
+
// expiry, LeaseOrphanSweeper), not idle until a session runs on it. A lease
|
|
655
|
+
// sets lastCmdExecutedAt when taken, so without this the sweeper freed a
|
|
656
|
+
// leased phone the new-command timeout later, before a slow first session
|
|
657
|
+
// had even started on it.
|
|
658
|
+
let leases = new Map();
|
|
659
|
+
if (busyDevices.length) {
|
|
660
|
+
try {
|
|
661
|
+
leases = yield (0, activeLeases_1.activeLeasesByDevice)();
|
|
662
|
+
}
|
|
663
|
+
catch (err) {
|
|
664
|
+
// Not knowing which phones are leased, sweeping would free all of them:
|
|
665
|
+
// their lastCmdExecutedAt is when the lease was taken. Next tick.
|
|
666
|
+
logger_1.default.warn(`Idle sweep skipped: could not read active leases: ${err}`);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
607
670
|
logger_1.default.debug(`Found ${busyDevices.length} device candidates to be released`);
|
|
608
671
|
for (const device of busyDevices) {
|
|
609
672
|
// need to keep this to make typescript happy. good thing tho.
|
|
@@ -622,11 +685,19 @@ function releaseBlockedDevices(newCommandTimeout) {
|
|
|
622
685
|
}
|
|
623
686
|
continue;
|
|
624
687
|
}
|
|
688
|
+
// Leased, with no session or hold on it: not idle.
|
|
689
|
+
if (leases.has((0, activeLeases_1.leaseKey)(device.udid, device.host)) &&
|
|
690
|
+
!device.claimSessionId &&
|
|
691
|
+
!device.session_id) {
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
625
694
|
const timeoutSeconds = device.newCommandTimeout != undefined ? device.newCommandTimeout : newCommandTimeout;
|
|
626
695
|
const timeSinceLastCmdExecuted = (currentEpoch - device.lastCmdExecutedAt) / 1000;
|
|
627
696
|
if (timeSinceLastCmdExecuted > timeoutSeconds) {
|
|
628
697
|
// unblock regardless of whether the device has session or not
|
|
629
698
|
logger_1.default.info(`Unblocking device ${device.udid} at host ${device.host} because it has been idle for ${timeSinceLastCmdExecuted} seconds`);
|
|
699
|
+
// Before anything below can release the phone (onSessionStopped too).
|
|
700
|
+
yield restoreIdleSessionNetwork((_a = device.claimSessionId) !== null && _a !== void 0 ? _a : device.session_id);
|
|
630
701
|
// Principal Protection: If this device has an active dashboard session, stop it properly
|
|
631
702
|
if (device.session_id) {
|
|
632
703
|
const sessionId = device.session_id;
|
|
@@ -642,7 +713,7 @@ function releaseBlockedDevices(newCommandTimeout) {
|
|
|
642
713
|
}
|
|
643
714
|
// Keyed on the session read here, so a release that loses the race to
|
|
644
715
|
// the session's own end (and the phone's next claim) frees nothing.
|
|
645
|
-
const claimant = (
|
|
716
|
+
const claimant = (_b = device.claimSessionId) !== null && _b !== void 0 ? _b : device.session_id;
|
|
646
717
|
yield (claimant
|
|
647
718
|
? (0, device_service_1.releaseSessionDevice)(device.udid, device.host, claimant)
|
|
648
719
|
: (0, device_service_1.unblockDevice)(device.udid, device.host)).catch((err) => logger_1.default.error(`Unable to release ${device.udid}: ${err}`));
|
|
@@ -650,6 +721,24 @@ function releaseBlockedDevices(newCommandTimeout) {
|
|
|
650
721
|
}
|
|
651
722
|
});
|
|
652
723
|
}
|
|
724
|
+
/**
|
|
725
|
+
* The network an idle session changed on its phone (profile, interceptor
|
|
726
|
+
* proxy), put back before the idle release frees the phone. Imported lazily,
|
|
727
|
+
* as this module's other session services are. Never throws.
|
|
728
|
+
*/
|
|
729
|
+
function restoreIdleSessionNetwork(sessionId) {
|
|
730
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
731
|
+
if (!sessionId)
|
|
732
|
+
return;
|
|
733
|
+
try {
|
|
734
|
+
const { PhoneNetworkRestore } = yield Promise.resolve().then(() => __importStar(require('./services/network/PhoneNetworkRestore')));
|
|
735
|
+
yield typedi_1.Container.get(PhoneNetworkRestore).restoreSession(sessionId, 'idle timeout');
|
|
736
|
+
}
|
|
737
|
+
catch (err) {
|
|
738
|
+
logger_1.default.warn(`Could not put back the network of idle session ${sessionId}: ${err}`);
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
}
|
|
653
742
|
function setupCronReleaseBlockedDevices(intervalMs, newCommandTimeoutSec) {
|
|
654
743
|
return __awaiter(this, void 0, void 0, function* () {
|
|
655
744
|
if (cronTimerToReleaseBlockedDevices) {
|
|
@@ -755,11 +844,15 @@ function setupCronCleanExpiredReservations(intervalMs) {
|
|
|
755
844
|
});
|
|
756
845
|
}
|
|
757
846
|
/**
|
|
758
|
-
* Sets up a cron job to purge older builds and sessions based on configuration
|
|
847
|
+
* Sets up a cron job to purge older builds and sessions based on configuration.
|
|
848
|
+
* The schedule is the one saved on the dashboard's Maintenance page, else the
|
|
849
|
+
* `buildCleanupSchedule` option. Calling it again replaces the job, which is
|
|
850
|
+
* how a schedule saved at runtime takes effect (`rescheduleCleanupBuilds`).
|
|
759
851
|
*/
|
|
760
852
|
function setupCronCleanupBuilds(pluginArgs) {
|
|
761
853
|
return __awaiter(this, void 0, void 0, function* () {
|
|
762
|
-
const {
|
|
854
|
+
const { loadEffectiveSettings } = yield Promise.resolve().then(() => __importStar(require('./services/settings/labSettings')));
|
|
855
|
+
const { buildCleanupSchedule } = yield loadEffectiveSettings(pluginArgs);
|
|
763
856
|
const { CleanupService } = yield Promise.resolve().then(() => __importStar(require('./services/CleanupService')));
|
|
764
857
|
const schedule = yield Promise.resolve().then(() => __importStar(require('node-schedule')));
|
|
765
858
|
const cleanupService = typedi_1.Container.get(CleanupService);
|
|
@@ -773,6 +866,19 @@ function setupCronCleanupBuilds(pluginArgs) {
|
|
|
773
866
|
}));
|
|
774
867
|
});
|
|
775
868
|
}
|
|
869
|
+
/**
|
|
870
|
+
* Put a cleanup schedule just saved on the Maintenance page to work: the old
|
|
871
|
+
* timer is cancelled and a new one installed. A server that runs no cleanup
|
|
872
|
+
* (a cloud-provider hub, see `setupMaintenanceCrons`) stays without one.
|
|
873
|
+
*/
|
|
874
|
+
function rescheduleCleanupBuilds(pluginArgs) {
|
|
875
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
876
|
+
var _a;
|
|
877
|
+
if ((_a = pluginArgs.cloud) === null || _a === void 0 ? void 0 : _a.cloudName)
|
|
878
|
+
return;
|
|
879
|
+
yield setupCronCleanupBuilds(pluginArgs);
|
|
880
|
+
});
|
|
881
|
+
}
|
|
776
882
|
let cronTimerSweepOrphanSessions;
|
|
777
883
|
let cronTimerReconcileDevices;
|
|
778
884
|
let cronTimerSelectorVerification;
|