@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
|
@@ -46,6 +46,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
46
46
|
};
|
|
47
47
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
48
|
exports.AI_SERVICE = exports.AIService = void 0;
|
|
49
|
+
exports.parseVisualVerdict = parseVisualVerdict;
|
|
49
50
|
const generative_ai_1 = require("@google/generative-ai");
|
|
50
51
|
const openai_1 = __importDefault(require("openai"));
|
|
51
52
|
const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
|
|
@@ -55,6 +56,41 @@ const fs = __importStar(require("fs"));
|
|
|
55
56
|
const path = __importStar(require("path"));
|
|
56
57
|
const config_1 = require("../config");
|
|
57
58
|
const CircuitBreaker_1 = require("./CircuitBreaker");
|
|
59
|
+
/** What GeminiProvider answers, instead of throwing, when the provider is rate-limited (429). */
|
|
60
|
+
const RATE_LIMITED = 'CONNECTION_OK_RATE_LIMITED';
|
|
61
|
+
/**
|
|
62
|
+
* A provider's answer to the assertion prompt, `{ "result": true|false,
|
|
63
|
+
* "reason": "..." }`, read leniently (code fences, text around the JSON,
|
|
64
|
+
* `message` for `reason`, "true"/"false" as strings) but never guessed: null
|
|
65
|
+
* when it holds no true/false `result`.
|
|
66
|
+
*/
|
|
67
|
+
function parseVisualVerdict(answer) {
|
|
68
|
+
const text = answer.replace(/```(?:json)?/gi, '').trim();
|
|
69
|
+
const start = text.indexOf('{');
|
|
70
|
+
const end = text.lastIndexOf('}');
|
|
71
|
+
if (start === -1 || end <= start)
|
|
72
|
+
return null;
|
|
73
|
+
let data;
|
|
74
|
+
try {
|
|
75
|
+
data = JSON.parse(text.slice(start, end + 1));
|
|
76
|
+
}
|
|
77
|
+
catch (_a) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const raw = data === null || data === void 0 ? void 0 : data.result;
|
|
81
|
+
const result = raw === true || raw === 'true' ? true : raw === false || raw === 'false' ? false : null;
|
|
82
|
+
if (result === null)
|
|
83
|
+
return null;
|
|
84
|
+
const reason = typeof data.reason === 'string'
|
|
85
|
+
? data.reason
|
|
86
|
+
: typeof data.message === 'string'
|
|
87
|
+
? data.message
|
|
88
|
+
: '';
|
|
89
|
+
return {
|
|
90
|
+
result,
|
|
91
|
+
message: reason.trim() || (result ? 'The condition holds.' : 'The condition does not hold.'),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
58
94
|
class GeminiProvider {
|
|
59
95
|
constructor(apiKey, modelName) {
|
|
60
96
|
this.genAI = new generative_ai_1.GoogleGenerativeAI(apiKey.trim());
|
|
@@ -365,6 +401,69 @@ Fix: Add a pre-emptive check for the location permission dialog or use the \`aut
|
|
|
365
401
|
}
|
|
366
402
|
});
|
|
367
403
|
}
|
|
404
|
+
/**
|
|
405
|
+
* Asks the provider whether `condition` holds on the screenshot
|
|
406
|
+
* (`xenon: assertVisualState`). Answers only with the provider's own
|
|
407
|
+
* verdict: with no provider, a failed call, a rate limit or an answer that
|
|
408
|
+
* isn't true or false it throws, saying the condition was not checked. A
|
|
409
|
+
* `false` there would pass a test asserting that something is absent.
|
|
410
|
+
*/
|
|
411
|
+
assertVisual(screenshotBase64, condition) {
|
|
412
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
413
|
+
var _a;
|
|
414
|
+
this.initializeProvider();
|
|
415
|
+
const notChecked = 'so the condition was not checked';
|
|
416
|
+
if (!this.provider) {
|
|
417
|
+
throw new Error(`No AI provider is configured, ${notChecked}. Set XENON_AI_PROVIDER and the provider's key.`);
|
|
418
|
+
}
|
|
419
|
+
const prompt = `
|
|
420
|
+
You are checking a screenshot of a mobile app for an automated test.
|
|
421
|
+
|
|
422
|
+
Condition: ${JSON.stringify(condition)}
|
|
423
|
+
|
|
424
|
+
Is the condition true of what the screenshot shows? Judge only what is visible on the screen.
|
|
425
|
+
Answer with JSON only, exactly: {"result": true or false, "reason": "one sentence on what you see"}
|
|
426
|
+
`.trim();
|
|
427
|
+
let answer;
|
|
428
|
+
try {
|
|
429
|
+
answer = yield this.callProvider(prompt, screenshotBase64);
|
|
430
|
+
}
|
|
431
|
+
catch (err) {
|
|
432
|
+
throw new Error(`The AI provider failed, ${notChecked}: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
433
|
+
}
|
|
434
|
+
if (answer === RATE_LIMITED) {
|
|
435
|
+
throw new Error(`The AI provider is rate-limited, ${notChecked}. Try again later.`);
|
|
436
|
+
}
|
|
437
|
+
const verdict = parseVisualVerdict(answer);
|
|
438
|
+
if (!verdict) {
|
|
439
|
+
const said = answer.replace(/\s+/g, ' ').trim().slice(0, 200);
|
|
440
|
+
throw new Error(`Xenon could not read the AI provider's answer as true or false, ${notChecked}. ` +
|
|
441
|
+
`It answered: ${JSON.stringify(said)}`);
|
|
442
|
+
}
|
|
443
|
+
return verdict;
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* A short description of the screen in the screenshot, for
|
|
448
|
+
* `xenon: analyzeScreen` and device control's Omni-Scan. Throws when there
|
|
449
|
+
* is no provider or the call fails, so the caller can say why there is none.
|
|
450
|
+
*/
|
|
451
|
+
describeScreen(screenshotBase64) {
|
|
452
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
453
|
+
this.initializeProvider();
|
|
454
|
+
if (!this.provider)
|
|
455
|
+
throw new Error('No AI provider is configured');
|
|
456
|
+
const prompt = `
|
|
457
|
+
You are looking at a screenshot of a mobile app, for a software tester.
|
|
458
|
+
Describe the screen in a few short sentences: which screen it is, its main elements and their state (for example filled in, empty, selected or disabled), and anything unexpected, such as an error message, a system dialog, a crash or a loading indicator.
|
|
459
|
+
Describe only what is visible. Answer in plain text.
|
|
460
|
+
`.trim();
|
|
461
|
+
const answer = yield this.callProvider(prompt, screenshotBase64);
|
|
462
|
+
if (answer === RATE_LIMITED)
|
|
463
|
+
throw new Error('The AI provider is rate-limited');
|
|
464
|
+
return answer;
|
|
465
|
+
});
|
|
466
|
+
}
|
|
368
467
|
/**
|
|
369
468
|
* Heals a broken locator using deep LLM reasoning (Tier 5)
|
|
370
469
|
*/
|
|
@@ -25,6 +25,7 @@ const logger_1 = __importDefault(require("../logger"));
|
|
|
25
25
|
const fs_1 = __importDefault(require("fs"));
|
|
26
26
|
const path_1 = __importDefault(require("path"));
|
|
27
27
|
const config_1 = require("../config");
|
|
28
|
+
const labSettings_1 = require("./settings/labSettings");
|
|
28
29
|
const recordingCleanup_1 = require("./recording/recordingCleanup");
|
|
29
30
|
/** Subdirectory holding composite (multi-device) outputs, keyed by group id. */
|
|
30
31
|
const GROUPS_DIR = '_groups';
|
|
@@ -34,6 +35,8 @@ let CleanupService = class CleanupService {
|
|
|
34
35
|
}
|
|
35
36
|
/**
|
|
36
37
|
* Orchestrates build + session + recording cleanup based on retention policy.
|
|
38
|
+
* The build retention window, the build cap and the asset purge are the ones
|
|
39
|
+
* saved on the dashboard's Maintenance page, else `pluginArgs`'.
|
|
37
40
|
*
|
|
38
41
|
* Phases:
|
|
39
42
|
* 1. Purge old builds (by age AND count cap) together with their sessions.
|
|
@@ -44,7 +47,9 @@ let CleanupService = class CleanupService {
|
|
|
44
47
|
*/
|
|
45
48
|
runCleanup(pluginArgs) {
|
|
46
49
|
return __awaiter(this, void 0, void 0, function* () {
|
|
47
|
-
|
|
50
|
+
// What the Maintenance page saved wins over the startup options, and is
|
|
51
|
+
// read here, at each run, so a change needs no restart. See labSettings.ts.
|
|
52
|
+
const { buildCleanupDays, buildCleanupMaxCount, deleteBuildAssets } = yield (0, labSettings_1.loadEffectiveSettings)(pluginArgs);
|
|
48
53
|
this.log.info(`Starting cleanup: Retention = ${buildCleanupDays} days, Max Builds = ${buildCleanupMaxCount}, Purge Assets = ${deleteBuildAssets}`);
|
|
49
54
|
const expirationDate = new Date();
|
|
50
55
|
expirationDate.setDate(expirationDate.getDate() - buildCleanupDays);
|
|
@@ -72,6 +72,10 @@ const SessionArchive_1 = require("./interceptor/SessionArchive");
|
|
|
72
72
|
const PortAllocator_1 = require("./PortAllocator");
|
|
73
73
|
const SocketServer_1 = require("./SocketServer");
|
|
74
74
|
const SocketEvents_1 = require("../enums/SocketEvents");
|
|
75
|
+
const adbForPhone_1 = require("./network/adbForPhone");
|
|
76
|
+
const PhoneNetworkLedger_1 = require("./network/PhoneNetworkLedger");
|
|
77
|
+
const phoneNetworkLock_1 = require("./network/phoneNetworkLock");
|
|
78
|
+
const xenonProxy_1 = require("./network/xenonProxy");
|
|
75
79
|
const DEFAULT_BUFFER_CAP = 1000;
|
|
76
80
|
const BODY_SPILL_THRESHOLD = 1024 * 1024;
|
|
77
81
|
let InterceptorService = class InterceptorService {
|
|
@@ -88,6 +92,10 @@ let InterceptorService = class InterceptorService {
|
|
|
88
92
|
isActive(sessionId) {
|
|
89
93
|
return this.states.has(sessionId);
|
|
90
94
|
}
|
|
95
|
+
/** The sessions whose traffic is captured on this server. */
|
|
96
|
+
sessionIds() {
|
|
97
|
+
return [...this.states.keys()];
|
|
98
|
+
}
|
|
91
99
|
start(sessionId, device, opts) {
|
|
92
100
|
return __awaiter(this, void 0, void 0, function* () {
|
|
93
101
|
var _a, _b, _c, _d;
|
|
@@ -143,7 +151,7 @@ let InterceptorService = class InterceptorService {
|
|
|
143
151
|
catch (err) {
|
|
144
152
|
this.logger.warn(`Cert install (${installMode}) failed for ${device.udid}: ${err.message}. HTTPS interception may not work.`);
|
|
145
153
|
}
|
|
146
|
-
yield
|
|
154
|
+
const previousProxy = yield this.pointPhoneAtProxy(sessionId, device.udid, host, port, routing.reverseEstablished);
|
|
147
155
|
const state = {
|
|
148
156
|
sessionId,
|
|
149
157
|
device,
|
|
@@ -155,6 +163,7 @@ let InterceptorService = class InterceptorService {
|
|
|
155
163
|
certInstalledFilename: certFilename,
|
|
156
164
|
startedAt: Date.now(),
|
|
157
165
|
reverseEstablished: routing.reverseEstablished,
|
|
166
|
+
previousProxy,
|
|
158
167
|
};
|
|
159
168
|
this.states.set(sessionId, state);
|
|
160
169
|
this.emit({ type: 'session_started', sessionId, port, host }, device.udid);
|
|
@@ -167,12 +176,7 @@ let InterceptorService = class InterceptorService {
|
|
|
167
176
|
if (!state)
|
|
168
177
|
return;
|
|
169
178
|
this.states.delete(sessionId);
|
|
170
|
-
|
|
171
|
-
yield this.getAndroidAdapter().clearProxy(state.device.udid);
|
|
172
|
-
}
|
|
173
|
-
catch (err) {
|
|
174
|
-
this.logger.warn(`Clear proxy failed for ${state.device.udid}: ${err.message}`);
|
|
175
|
-
}
|
|
179
|
+
yield this.putPhoneProxyBack(state);
|
|
176
180
|
if (state.reverseEstablished) {
|
|
177
181
|
try {
|
|
178
182
|
yield this.getAndroidAdapter().removeReverse(state.device.udid, state.port);
|
|
@@ -284,15 +288,68 @@ let InterceptorService = class InterceptorService {
|
|
|
284
288
|
requests: state.buffer.list(),
|
|
285
289
|
};
|
|
286
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* Points the whole phone at the session's proxy, and returns the proxy the
|
|
293
|
+
* phone had (null: none) for stop() to put back. The change is written down
|
|
294
|
+
* before it is made, so a restart after a crash can undo it
|
|
295
|
+
* (PhoneNetworkLedger, PhoneNetworkRestore).
|
|
296
|
+
*
|
|
297
|
+
* A previous proxy that is a capture an earlier run left behind (this
|
|
298
|
+
* machine, a capture port, nothing answering) is not the phone's own: it
|
|
299
|
+
* counts as none, so the end of this session doesn't put a dead proxy back.
|
|
300
|
+
*/
|
|
301
|
+
pointPhoneAtProxy(sessionId, udid, host, port, reverseEstablished) {
|
|
302
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
303
|
+
const adapter = this.getAndroidAdapter();
|
|
304
|
+
const setting = `${host}:${port}`;
|
|
305
|
+
return yield (0, phoneNetworkLock_1.withPhoneNetworkLock)(udid, () => __awaiter(this, void 0, void 0, function* () {
|
|
306
|
+
var _a;
|
|
307
|
+
let previous = null;
|
|
308
|
+
try {
|
|
309
|
+
const read = yield adapter.readProxy(udid);
|
|
310
|
+
const leftover = read === setting ||
|
|
311
|
+
(yield (0, xenonProxy_1.isDeadCaptureProxy)(read, typedi_1.Container.get(PortAllocator_1.PortAllocator).rangeOf('proxy')));
|
|
312
|
+
if (leftover) {
|
|
313
|
+
this.logger.info(`[${sessionId}] ${udid} had a proxy left by an earlier capture (${read})`);
|
|
314
|
+
}
|
|
315
|
+
previous = (0, xenonProxy_1.isUnsetProxy)(read) || leftover ? null : read;
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
this.logger.warn(`Could not read the proxy of ${udid}: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
319
|
+
}
|
|
320
|
+
yield typedi_1.Container.get(PhoneNetworkLedger_1.PhoneNetworkLedger).note(sessionId, udid, {
|
|
321
|
+
proxy: Object.assign({ set: setting, previous }, (reverseEstablished ? { reversePort: port } : {})),
|
|
322
|
+
});
|
|
323
|
+
yield adapter.setProxy(udid, host, port);
|
|
324
|
+
return previous;
|
|
325
|
+
}));
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Puts back the proxy the phone had before the session. A phone that can't
|
|
330
|
+
* be reached keeps the change written down, and is put back at its next
|
|
331
|
+
* session or at this server's next start.
|
|
332
|
+
*/
|
|
333
|
+
putPhoneProxyBack(state) {
|
|
334
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
335
|
+
const udid = state.device.udid;
|
|
336
|
+
yield (0, phoneNetworkLock_1.withPhoneNetworkLock)(udid, () => __awaiter(this, void 0, void 0, function* () {
|
|
337
|
+
var _a;
|
|
338
|
+
try {
|
|
339
|
+
yield this.getAndroidAdapter().restoreProxy(udid, state.previousProxy);
|
|
340
|
+
yield typedi_1.Container.get(PhoneNetworkLedger_1.PhoneNetworkLedger).settle(state.sessionId, 'proxy');
|
|
341
|
+
}
|
|
342
|
+
catch (err) {
|
|
343
|
+
this.logger.warn(`Could not put back the proxy of ${udid} after session ${state.sessionId}: ` +
|
|
344
|
+
`${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}. Xenon tries again at the phone's next session and when it restarts.`);
|
|
345
|
+
}
|
|
346
|
+
}));
|
|
347
|
+
});
|
|
348
|
+
}
|
|
287
349
|
getAndroidAdapter() {
|
|
288
350
|
if (this.androidAdapter)
|
|
289
351
|
return this.androidAdapter;
|
|
290
|
-
|
|
291
|
-
const { default: AndroidDeviceManager } = yield Promise.resolve().then(() => __importStar(require('../device-managers/AndroidDeviceManager')));
|
|
292
|
-
const mgr = typedi_1.Container.get(AndroidDeviceManager);
|
|
293
|
-
return yield mgr.getAdbForDevice(udid);
|
|
294
|
-
});
|
|
295
|
-
this.androidAdapter = new AndroidProxyAdapter_1.AndroidProxyAdapter(provider);
|
|
352
|
+
this.androidAdapter = new AndroidProxyAdapter_1.AndroidProxyAdapter(adbForPhone_1.adbForPhone);
|
|
296
353
|
return this.androidAdapter;
|
|
297
354
|
}
|
|
298
355
|
// Establishes how the device will reach the host-side MITM proxy.
|
|
@@ -1,43 +1,10 @@
|
|
|
1
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
2
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
19
3
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
20
4
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
21
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;
|
|
22
6
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
23
7
|
};
|
|
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
8
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
42
9
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
43
10
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -54,6 +21,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
54
21
|
exports.NetworkConditioningService = void 0;
|
|
55
22
|
const typedi_1 = require("typedi");
|
|
56
23
|
const logger_1 = __importDefault(require("../logger"));
|
|
24
|
+
const adbForPhone_1 = require("./network/adbForPhone");
|
|
25
|
+
const AndroidRadios_1 = require("./network/AndroidRadios");
|
|
26
|
+
const PhoneNetworkLedger_1 = require("./network/PhoneNetworkLedger");
|
|
27
|
+
const phoneNetworkLock_1 = require("./network/phoneNetworkLock");
|
|
57
28
|
const NETWORK_PROFILES = {
|
|
58
29
|
Normal: null,
|
|
59
30
|
'4G': { latencyMs: 20, downloadKbps: 15000, uploadKbps: 7500 },
|
|
@@ -62,26 +33,47 @@ const NETWORK_PROFILES = {
|
|
|
62
33
|
Offline: { latencyMs: 0, downloadKbps: 0, uploadKbps: 0 },
|
|
63
34
|
};
|
|
64
35
|
/**
|
|
65
|
-
*
|
|
36
|
+
* A session's network profile (`xe:network_profile`).
|
|
37
|
+
*
|
|
38
|
+
* Every profile's delay is added to each of the session's commands by the
|
|
39
|
+
* session gateway (getLatency). On an Android phone `Offline` also turns
|
|
40
|
+
* mobile data and Wi-Fi off, and `Normal` turns them on; 4G, 3G and Edge
|
|
41
|
+
* change nothing on the phone. An iPhone, real or simulated, gets the delay
|
|
42
|
+
* only: Xcode has no command to change a simulator's network (`xcrun simctl`
|
|
43
|
+
* has no `network` subcommand in Xcode 26), and a real iPhone's can't be
|
|
44
|
+
* changed from a Mac.
|
|
45
|
+
*
|
|
46
|
+
* What `Offline` changed is undone when the session ends, however it ends
|
|
47
|
+
* (PhoneNetworkRestore), and written down meanwhile (PhoneNetworkLedger) so a
|
|
48
|
+
* restart can undo it after a crash.
|
|
66
49
|
*/
|
|
67
50
|
let NetworkConditioningService = class NetworkConditioningService {
|
|
68
51
|
constructor() {
|
|
69
52
|
this.log = logger_1.default.scope('NetworkConditioning');
|
|
70
53
|
this.activeConditions = new Map();
|
|
54
|
+
this.radios = new AndroidRadios_1.AndroidRadios(adbForPhone_1.adbForPhone);
|
|
55
|
+
this.toldAboutIPhones = false;
|
|
71
56
|
}
|
|
72
57
|
/**
|
|
73
58
|
* Applies a network profile to a session/device
|
|
74
59
|
*/
|
|
75
60
|
applyProfile(sessionId, device, profile) {
|
|
76
61
|
return __awaiter(this, void 0, void 0, function* () {
|
|
62
|
+
var _a;
|
|
77
63
|
this.log.info(`Applying network profile '${profile}' to session ${sessionId} on device ${device.udid}`);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (device.platform === 'android') {
|
|
81
|
-
|
|
64
|
+
const condition = { profile, device };
|
|
65
|
+
this.activeConditions.set(sessionId, condition);
|
|
66
|
+
if (((_a = device.platform) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'android') {
|
|
67
|
+
if (profile === 'Offline')
|
|
68
|
+
yield this.takeOffline(sessionId, condition);
|
|
69
|
+
else if (profile === 'Normal')
|
|
70
|
+
yield this.turnOnline(device.udid);
|
|
71
|
+
// 4G, 3G and Edge: the delay only.
|
|
82
72
|
}
|
|
83
|
-
else if (
|
|
84
|
-
|
|
73
|
+
else if (!this.toldAboutIPhones) {
|
|
74
|
+
this.toldAboutIPhones = true;
|
|
75
|
+
this.log.info('Network profiles on iPhones and iOS simulators only delay each command: the ' +
|
|
76
|
+
"device's own network isn't changed, so Offline leaves it online.");
|
|
85
77
|
}
|
|
86
78
|
});
|
|
87
79
|
}
|
|
@@ -89,70 +81,75 @@ let NetworkConditioningService = class NetworkConditioningService {
|
|
|
89
81
|
* Gets the active profile for a session
|
|
90
82
|
*/
|
|
91
83
|
getProfile(sessionId) {
|
|
92
|
-
|
|
84
|
+
var _a;
|
|
85
|
+
return (_a = this.activeConditions.get(sessionId)) === null || _a === void 0 ? void 0 : _a.profile;
|
|
86
|
+
}
|
|
87
|
+
/** Whether the session has a profile on this server. */
|
|
88
|
+
has(sessionId) {
|
|
89
|
+
return this.activeConditions.has(sessionId);
|
|
90
|
+
}
|
|
91
|
+
/** The sessions with a profile on this server. */
|
|
92
|
+
sessionIds() {
|
|
93
|
+
return [...this.activeConditions.keys()];
|
|
93
94
|
}
|
|
94
95
|
/**
|
|
95
|
-
*
|
|
96
|
+
* Ends the session's profile and undoes what it changed on the phone, once:
|
|
97
|
+
* the entry is taken before anything is awaited, so a second call finds
|
|
98
|
+
* nothing to do. A phone that can't be reached keeps its record in the
|
|
99
|
+
* ledger, and is put back at its next session or this server's next start.
|
|
96
100
|
*/
|
|
97
|
-
reset(sessionId
|
|
101
|
+
reset(sessionId) {
|
|
98
102
|
return __awaiter(this, void 0, void 0, function* () {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
const condition = this.activeConditions.get(sessionId);
|
|
104
|
+
if (!condition)
|
|
105
|
+
return;
|
|
106
|
+
this.activeConditions.delete(sessionId);
|
|
107
|
+
this.log.info(`Resetting network conditions for session ${sessionId}`);
|
|
108
|
+
const prior = condition.offlinePrior;
|
|
109
|
+
if (!prior)
|
|
110
|
+
return;
|
|
111
|
+
const udid = condition.device.udid;
|
|
112
|
+
yield (0, phoneNetworkLock_1.withPhoneNetworkLock)(udid, () => __awaiter(this, void 0, void 0, function* () {
|
|
113
|
+
try {
|
|
114
|
+
yield this.radios.turnOn(udid, AndroidRadios_1.AndroidRadios.toRestore(prior));
|
|
115
|
+
yield typedi_1.Container.get(PhoneNetworkLedger_1.PhoneNetworkLedger).settle(sessionId, 'offline');
|
|
116
|
+
this.log.info(`📱 Android: network put back on ${udid} after session ${sessionId}`);
|
|
104
117
|
}
|
|
105
|
-
|
|
106
|
-
|
|
118
|
+
catch (e) {
|
|
119
|
+
this.log.warn(`⚠️ Could not put the network back on ${udid} after session ${sessionId}: ${e === null || e === void 0 ? void 0 : e.message}. ` +
|
|
120
|
+
"Xenon tries again at the phone's next session and when it restarts.");
|
|
107
121
|
}
|
|
108
|
-
}
|
|
122
|
+
}));
|
|
109
123
|
});
|
|
110
124
|
}
|
|
111
|
-
|
|
125
|
+
takeOffline(sessionId, condition) {
|
|
112
126
|
return __awaiter(this, void 0, void 0, function* () {
|
|
127
|
+
const udid = condition.device.udid;
|
|
113
128
|
try {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
yield
|
|
121
|
-
}
|
|
122
|
-
else if (profile === 'Normal') {
|
|
123
|
-
this.log.info(`📱 Android: Enabling data for ${udid}`);
|
|
124
|
-
yield execAsync(`adb -s ${udid} shell svc data enable`);
|
|
125
|
-
yield execAsync(`adb -s ${udid} shell svc wifi enable`);
|
|
126
|
-
}
|
|
127
|
-
// Latency is handled by the proxy
|
|
129
|
+
yield (0, phoneNetworkLock_1.withPhoneNetworkLock)(udid, () => __awaiter(this, void 0, void 0, function* () {
|
|
130
|
+
const prior = yield this.radios.read(udid);
|
|
131
|
+
// Written down before the change: a crash after it must still find it.
|
|
132
|
+
yield typedi_1.Container.get(PhoneNetworkLedger_1.PhoneNetworkLedger).note(sessionId, udid, { offline: prior });
|
|
133
|
+
condition.offlinePrior = prior;
|
|
134
|
+
this.log.info(`📱 Android: turning mobile data and Wi-Fi off on ${udid}`);
|
|
135
|
+
yield this.radios.turnOff(udid);
|
|
136
|
+
}));
|
|
128
137
|
}
|
|
129
138
|
catch (e) {
|
|
130
|
-
this.log.warn(`⚠️ Failed to apply Android conditioning for ${udid}: ${e.message}`);
|
|
139
|
+
this.log.warn(`⚠️ Failed to apply Android conditioning for ${udid}: ${e === null || e === void 0 ? void 0 : e.message}`);
|
|
131
140
|
}
|
|
132
141
|
});
|
|
133
142
|
}
|
|
134
|
-
|
|
143
|
+
turnOnline(udid) {
|
|
135
144
|
return __awaiter(this, void 0, void 0, function* () {
|
|
136
145
|
try {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// Profiles: wifi, 3g, edge, etc. (depends on OS version)
|
|
142
|
-
let status = 'none';
|
|
143
|
-
if (profile === '3G')
|
|
144
|
-
status = '3g';
|
|
145
|
-
if (profile === 'Edge')
|
|
146
|
-
status = 'edge';
|
|
147
|
-
if (profile === 'Offline')
|
|
148
|
-
status = 'off';
|
|
149
|
-
if (profile === 'Normal')
|
|
150
|
-
status = 'none';
|
|
151
|
-
this.log.info(`🍎 iOS Sim: Setting network to ${status} for ${udid}`);
|
|
152
|
-
yield execAsync(`xcrun simctl network ${udid} status ${status}`);
|
|
146
|
+
yield (0, phoneNetworkLock_1.withPhoneNetworkLock)(udid, () => __awaiter(this, void 0, void 0, function* () {
|
|
147
|
+
this.log.info(`📱 Android: turning mobile data and Wi-Fi on on ${udid}`);
|
|
148
|
+
yield this.radios.turnOn(udid, { data: true, wifi: true });
|
|
149
|
+
}));
|
|
153
150
|
}
|
|
154
151
|
catch (e) {
|
|
155
|
-
this.log.warn(`⚠️ Failed to apply
|
|
152
|
+
this.log.warn(`⚠️ Failed to apply Android conditioning for ${udid}: ${e === null || e === void 0 ? void 0 : e.message}`);
|
|
156
153
|
}
|
|
157
154
|
});
|
|
158
155
|
}
|
|
@@ -161,10 +158,10 @@ let NetworkConditioningService = class NetworkConditioningService {
|
|
|
161
158
|
*/
|
|
162
159
|
getLatency(sessionId) {
|
|
163
160
|
var _a;
|
|
164
|
-
const
|
|
165
|
-
if (!
|
|
161
|
+
const condition = this.activeConditions.get(sessionId);
|
|
162
|
+
if (!condition)
|
|
166
163
|
return 0;
|
|
167
|
-
return ((_a = NETWORK_PROFILES[profile]) === null || _a === void 0 ? void 0 : _a.latencyMs) || 0;
|
|
164
|
+
return ((_a = NETWORK_PROFILES[condition.profile]) === null || _a === void 0 ? void 0 : _a.latencyMs) || 0;
|
|
168
165
|
}
|
|
169
166
|
};
|
|
170
167
|
exports.NetworkConditioningService = NetworkConditioningService;
|
|
@@ -26,9 +26,14 @@ const axios_1 = __importDefault(require("axios"));
|
|
|
26
26
|
const logger_1 = __importDefault(require("../logger"));
|
|
27
27
|
const prisma_service_1 = require("../data-service/prisma-service");
|
|
28
28
|
const typedi_1 = require("typedi");
|
|
29
|
+
const webhookEvents_1 = require("./webhookEvents");
|
|
30
|
+
/** How many sessions' failure notices are remembered, so a session ending twice sends once. */
|
|
31
|
+
const NOTIFIED_SESSIONS_KEPT = 2000;
|
|
29
32
|
let NotificationService = class NotificationService {
|
|
30
33
|
constructor(prisma) {
|
|
31
34
|
this.prisma = prisma;
|
|
35
|
+
/** Sessions whose failure was sent, oldest first (a Set keeps insertion order). */
|
|
36
|
+
this.notifiedSessions = new Set();
|
|
32
37
|
}
|
|
33
38
|
getConfigs() {
|
|
34
39
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -72,31 +77,41 @@ let NotificationService = class NotificationService {
|
|
|
72
77
|
});
|
|
73
78
|
}
|
|
74
79
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
80
|
+
* Tells the webhooks subscribed to `session_failed` that a session failed.
|
|
81
|
+
*
|
|
82
|
+
* Once per session however many times it ends: a driver crash and the
|
|
83
|
+
* client's own delete both end it, as do a heartbeat timeout and a delete.
|
|
84
|
+
* Remembered by id in memory, so a restart forgets it, which is harmless:
|
|
85
|
+
* a session does not end again after one.
|
|
86
|
+
*/
|
|
87
|
+
notifySessionFailed(session) {
|
|
88
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
89
|
+
if (this.notifiedSessions.has(session.id))
|
|
90
|
+
return;
|
|
91
|
+
this.notifiedSessions.add(session.id);
|
|
92
|
+
if (this.notifiedSessions.size > NOTIFIED_SESSIONS_KEPT) {
|
|
93
|
+
const oldest = this.notifiedSessions.values().next().value;
|
|
94
|
+
this.notifiedSessions.delete(oldest);
|
|
95
|
+
}
|
|
96
|
+
yield this.dispatchEvent('session_failed', (0, webhookEvents_1.sessionFailedPayload)(session));
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Sends a sample of `event` to a webhook the way a real one would be sent,
|
|
101
|
+
* and fails if the delivery does: "Send test" used to say it worked
|
|
102
|
+
* whatever happened, and ignored the webhook's type and template.
|
|
78
103
|
*/
|
|
79
104
|
sendTest(url_1) {
|
|
80
|
-
return __awaiter(this, arguments, void 0, function* (url, type = 'slack', payloadTemplate) {
|
|
81
|
-
yield this.sendToWebhook({ url, type, payloadTemplate: payloadTemplate !== null && payloadTemplate !== void 0 ? payloadTemplate : null },
|
|
105
|
+
return __awaiter(this, arguments, void 0, function* (url, type = 'slack', payloadTemplate, event = 'device_new') {
|
|
106
|
+
yield this.sendToWebhook({ url, type, payloadTemplate: payloadTemplate !== null && payloadTemplate !== void 0 ? payloadTemplate : null }, event, webhookEvents_1.WEBHOOK_EVENTS[event].sample());
|
|
82
107
|
});
|
|
83
108
|
}
|
|
84
109
|
/** Delivers one event. Throws when the webhook refuses or can't be reached. */
|
|
85
110
|
sendToWebhook(config, eventType, payload) {
|
|
86
111
|
return __awaiter(this, void 0, void 0, function* () {
|
|
87
|
-
//
|
|
112
|
+
// A custom template, if there is one, replaces the built-in message.
|
|
88
113
|
if (config.payloadTemplate) {
|
|
89
|
-
const
|
|
90
|
-
// JSON if the template renders to JSON, otherwise sent as text. Only
|
|
91
|
-
// the parse may fall back: one catch around both used to post again,
|
|
92
|
-
// as text, when the JSON delivery itself failed.
|
|
93
|
-
let body;
|
|
94
|
-
try {
|
|
95
|
-
body = JSON.parse(substitutedBody);
|
|
96
|
-
}
|
|
97
|
-
catch (_a) {
|
|
98
|
-
body = { text: substitutedBody };
|
|
99
|
-
}
|
|
114
|
+
const body = (0, webhookEvents_1.renderTemplate)(config.payloadTemplate, Object.assign({ eventType }, payload));
|
|
100
115
|
yield axios_1.default.post(config.url, body);
|
|
101
116
|
return;
|
|
102
117
|
}
|
|
@@ -109,17 +124,6 @@ let NotificationService = class NotificationService {
|
|
|
109
124
|
}
|
|
110
125
|
});
|
|
111
126
|
}
|
|
112
|
-
// Principal Logic: Recursive variable substitution with {{key}} support
|
|
113
|
-
substituteTemplate(template, data) {
|
|
114
|
-
return template.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
|
|
115
|
-
const keys = key.trim().split('.');
|
|
116
|
-
let value = data;
|
|
117
|
-
for (const k of keys) {
|
|
118
|
-
value = value ? value[k] : undefined;
|
|
119
|
-
}
|
|
120
|
-
return value !== undefined ? String(value) : match;
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
127
|
sendSlackMessage(url, eventType, payload) {
|
|
124
128
|
return __awaiter(this, void 0, void 0, function* () {
|
|
125
129
|
var _a, _b, _c;
|
|
@@ -131,7 +135,10 @@ let NotificationService = class NotificationService {
|
|
|
131
135
|
color = '#ff0000';
|
|
132
136
|
break;
|
|
133
137
|
case 'session_failed':
|
|
134
|
-
text =
|
|
138
|
+
text =
|
|
139
|
+
`❌ *Session Failed*: ${payload.sessionId}\n` +
|
|
140
|
+
`Reason: ${payload.failureReason || 'no reason recorded'}\n` +
|
|
141
|
+
`Device: ${payload.deviceName || payload.udid} (${payload.udid})`;
|
|
135
142
|
color = '#ff0000';
|
|
136
143
|
break;
|
|
137
144
|
case 'device_new':
|
|
@@ -25,6 +25,7 @@ const logger_1 = __importDefault(require("../logger"));
|
|
|
25
25
|
const event_manager_1 = require("../dashboard/event-manager");
|
|
26
26
|
const SessionStatus_1 = require("../types/SessionStatus");
|
|
27
27
|
const device_service_1 = require("../data-service/device-service");
|
|
28
|
+
const PhoneNetworkRestore_1 = require("./network/PhoneNetworkRestore");
|
|
28
29
|
let OrphanSweeper = class OrphanSweeper {
|
|
29
30
|
constructor() {
|
|
30
31
|
this.log = logger_1.default.scope('OrphanSweeper');
|
|
@@ -69,6 +70,10 @@ let OrphanSweeper = class OrphanSweeper {
|
|
|
69
70
|
endTime: new Date(),
|
|
70
71
|
},
|
|
71
72
|
});
|
|
73
|
+
// A session this process still drove has its phone's network put
|
|
74
|
+
// back before the phone is released; for a previous process's it is
|
|
75
|
+
// the ledger's, at boot (PhoneNetworkRestore).
|
|
76
|
+
yield typedi_1.Container.get(PhoneNetworkRestore_1.PhoneNetworkRestore).restoreSession(s.id, 'heartbeat timeout');
|
|
72
77
|
// This session's claim only, not every row with its udid: the phone
|
|
73
78
|
// may be another session's by now (deviceClaims.ts).
|
|
74
79
|
yield (0, device_service_1.releaseSessionDevices)(s.id);
|
|
@@ -55,6 +55,11 @@ let PortAllocator = class PortAllocator {
|
|
|
55
55
|
configure(overrides) {
|
|
56
56
|
this.ranges = Object.assign(Object.assign({}, DEFAULT_RANGES), overrides);
|
|
57
57
|
}
|
|
58
|
+
/** The ports leased for `purpose`, first and last included. */
|
|
59
|
+
rangeOf(purpose) {
|
|
60
|
+
const [start, end] = this.ranges[purpose];
|
|
61
|
+
return [start, end];
|
|
62
|
+
}
|
|
58
63
|
acquire(purpose_1, udid_1) {
|
|
59
64
|
return __awaiter(this, arguments, void 0, function* (purpose, udid, opts = {}) {
|
|
60
65
|
var _a;
|