@xenon-device-management/xenon 2.16.0 → 2.17.1
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 +2 -2
- package/lib/package.json +2 -3
- package/lib/public/assets/{AnnotationOverlay-2W4JvEyZ.js → AnnotationOverlay-CB8njQvS.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-GmckX_7r.js → ApiKeyGate-B-804QBS.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-BsDvfjR1.js +2 -0
- package/lib/public/assets/{EmptyState-7hETKEWb.js → EmptyState-WzLw1cEV.js} +1 -1
- package/lib/public/assets/{FieldGroup-GOgssFEP.js → FieldGroup-CaQNXgcE.js} +1 -1
- package/lib/public/assets/{FilterMenu-Yvf6RGMd.js → FilterMenu-C9gAiypL.js} +1 -1
- package/lib/public/assets/{Menu-KrPnfdsf.js → Menu-CfTti3rw.js} +1 -1
- package/lib/public/assets/{Modal-C6WjZFeI.js → Modal-BcZUFhDm.js} +1 -1
- package/lib/public/assets/{RecordingPage-CSRIhZEh.js → RecordingPage-B_pbl3UE.js} +1 -1
- package/lib/public/assets/{RecordingsPage-CB8ST3nE.js → RecordingsPage-BkVhplE3.js} +1 -1
- package/lib/public/assets/{SegmentedControl-C9o90KId.js → SegmentedControl-CDLMLNl5.js} +1 -1
- package/lib/public/assets/{SettingCard-CPm3w9l2.js → SettingCard-Lyw5CB1F.js} +1 -1
- package/lib/public/assets/{Table-DwIgERRt.js → Table-CK414bOU.js} +1 -1
- package/lib/public/assets/{activity-DrLbHifn.js → activity-pEyAdE-M.js} +1 -1
- package/lib/public/assets/{ai-settings-DU_Ls06P.js → ai-settings-CIhS7MMq.js} +1 -1
- package/lib/public/assets/{api-keys-CWYdEzJ_.js → api-keys-D2J44Ebv.js} +1 -1
- package/lib/public/assets/{apps-BqNVdln7.js → apps-CCV3RZY8.js} +1 -1
- package/lib/public/assets/{arrow-left-BtB0_Jg_.js → arrow-left-DaEUmERk.js} +1 -1
- package/lib/public/assets/{arrow-right-C8NhdtyN.js → arrow-right-CnRXHXit.js} +1 -1
- package/lib/public/assets/{arrow-up-right-gMg0ffvv.js → arrow-up-right-BGCS7zhi.js} +1 -1
- package/lib/public/assets/{auth-shell-CiYZ3N4M.js → auth-shell-CpW1zsxD.js} +2 -2
- package/lib/public/assets/{builds-page-EaaqDmLN.js → builds-page-B-xdzjMx.js} +1 -1
- package/lib/public/assets/{button-DDau4Axv.js → button-B5QkOVa4.js} +1 -1
- package/lib/public/assets/{calendar-DQ83B1ed.js → calendar-gX_1EKue.js} +1 -1
- package/lib/public/assets/{check-K1ovTQam.js → check-MIM4THvH.js} +1 -1
- package/lib/public/assets/{chevron-right-Biq00pMt.js → chevron-right-BZIp3-9t.js} +1 -1
- package/lib/public/assets/{circle-check-CBv88cH7.js → circle-check-Crt-G_uy.js} +1 -1
- package/lib/public/assets/{circle-x-Bmltailn.js → circle-x-C8GvvXVt.js} +1 -1
- package/lib/public/assets/{clock-BPrld-3h.js → clock-MFKV4qGa.js} +1 -1
- package/lib/public/assets/{copy-t0QB14JI.js → copy-BU-Z7mBK.js} +1 -1
- package/lib/public/assets/device-explorer-D-78Twa0.js +282 -0
- package/lib/public/assets/{download-BroHxNOh.js → download-DIoLCCYv.js} +1 -1
- package/lib/public/assets/{forgot-password-Bjmf49i6.js → forgot-password-vNYlPnia.js} +1 -1
- package/lib/public/assets/{h264Stream-DeD__6wa.js → h264Stream-BLcSplMU.js} +1 -1
- package/lib/public/assets/{index-B0gQkEg0.js → index-BpqyclNY.js} +2 -2
- package/lib/public/assets/{index-B0JeuZbg.js → index-Hl1OL-k_.js} +1 -1
- package/lib/public/assets/{index-72oF89LJ.js → index-QbukVVrl.js} +1 -1
- package/lib/public/assets/{input-DIc4MSC7.js → input-DHH3fHer.js} +1 -1
- package/lib/public/assets/{line-chart-BcWm2vW8.js → line-chart-PdViEphw.js} +1 -1
- package/lib/public/assets/{list-checks-B06IJJad.js → list-checks-DI_heIg9.js} +1 -1
- package/lib/public/assets/{lock-pVLvnN30.js → lock-DVP_dL6J.js} +1 -1
- package/lib/public/assets/{login-CN77vy--.js → login-B8vtTDcr.js} +1 -1
- package/lib/public/assets/{maintenance-settings-CSdzQSkY.js → maintenance-settings-BMa5EfgI.js} +1 -1
- package/lib/public/assets/{monitor-BtrN5IkI.js → monitor-CtzX_RkD.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CNsQfo6w.js → mouse-pointer-2-BA09sdS9.js} +1 -1
- package/lib/public/assets/{network-BSfA8ZxG.js → network-B_F9Nox-.js} +1 -1
- package/lib/public/assets/{overview-BfTebCK5.js → overview-kWCYMHMV.js} +1 -1
- package/lib/public/assets/{page-header-D8KUX6Qg.js → page-header-CAbH1O1a.js} +1 -1
- package/lib/public/assets/{play-Cvc8xzic.js → play-sg95hLqA.js} +1 -1
- package/lib/public/assets/{plus-C9Zj84UF.js → plus-BT9mk2gj.js} +1 -1
- package/lib/public/assets/{profile-page-Bi27STgs.js → profile-page-DZOz5Ypc.js} +1 -1
- package/lib/public/assets/{reset-password-CPsOuwSu.js → reset-password-CqeImlEO.js} +1 -1
- package/lib/public/assets/{runbook-page-DEQ-DBOX.js → runbook-page-U_8TSysj.js} +1 -1
- package/lib/public/assets/{select-BWpnhWXj.js → select-Cx4JDzsR.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BefSCSpe.js → selector-detail-redirect-DiMRe7DW.js} +1 -1
- package/lib/public/assets/{selector-health-page-MSk96Zz7.js → selector-health-page-CKR5URll.js} +1 -1
- package/lib/public/assets/{session-detail-page-DPyBNef_.js → session-detail-page-DuIVzTW_.js} +1 -1
- package/lib/public/assets/{settings-V6x-cXwy.js → settings-B5fuvgTP.js} +1 -1
- package/lib/public/assets/{stat-tile-Duex-GZD.js → stat-tile-CtHX1dkh.js} +1 -1
- package/lib/public/assets/{tablet-BBcoqBmL.js → tablet-DCGD0GQt.js} +1 -1
- package/lib/public/assets/{teams-ts8p5rEh.js → teams-MzlELlw9.js} +1 -1
- package/lib/public/assets/{trash-2-CZ2ew6AZ.js → trash-2-BIRm2KVs.js} +1 -1
- package/lib/public/assets/{upload-Dv99y_le.js → upload-C-lgyswH.js} +1 -1
- package/lib/public/assets/{use-builds-data-DrYC8JCF.js → use-builds-data-IOydXA8d.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-C7QkBNkP.js → use-password-reset-mode-CIVXG7Me.js} +1 -1
- package/lib/public/assets/{users-gJ5Y01yo.js → users-hwK5XGIy.js} +1 -1
- package/lib/public/assets/{users-D7tKMESH.js → users-ivNCjeus.js} +1 -1
- package/lib/public/assets/{video-off-CqZVuZGs.js → video-off-DvcIEsdZ.js} +1 -1
- package/lib/public/assets/{webhook-settings-DI5DSW99.js → webhook-settings-DukgCe1u.js} +1 -1
- package/lib/public/assets/{zap-ClcHFxfo.js → zap-BE9gacFj.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +1 -1
- package/lib/src/app/index.js +2 -0
- package/lib/src/app/openapi/control.yaml +69 -4
- package/lib/src/app/openapi/grid.yaml +102 -0
- package/lib/src/app/routers/control.js +24 -5
- package/lib/src/dashboard/event-manager.js +5 -8
- package/lib/src/device-managers/ios/IOSStreamService.js +147 -18
- package/lib/src/device-utils.js +61 -24
- package/lib/src/gateway/internalCall.js +33 -5
- package/lib/src/gateway/nodeAsk.js +57 -0
- package/lib/src/gateway/nodeSessionDeviceLogs.js +35 -0
- package/lib/src/gateway/sessionGateway.js +9 -4
- package/lib/src/helpers/index.js +9 -6
- package/lib/src/interceptors/CommandInterceptor.js +10 -0
- package/lib/src/plugin.js +4 -1
- package/lib/src/scripts/initialize-database.js +0 -6
- package/lib/src/scripts/run-migrations.js +123 -880
- package/lib/src/services/ServerManager.js +25 -4
- package/lib/src/services/SessionLifecycleService.js +20 -5
- package/lib/src/services/UserService.js +8 -3
- package/lib/src/services/interceptor/CertManager.js +50 -16
- package/lib/src/services/logcat/NodeDeviceLogStore.js +99 -0
- package/lib/src/services/logcat/NodeDeviceLogsCollector.js +164 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +199 -44
- package/lib/src/services/logcat/nodeDeviceLogs.js +91 -0
- package/lib/src/services/metrics/nodeMetrics.js +20 -49
- package/lib/src/sessions/RemoteSession.js +11 -2
- package/lib/src/sessions/SessionManager.js +28 -1
- package/lib/src/sessions/appiumUmbrella.js +11 -0
- package/lib/src/sessions/sessionMemory.js +11 -2
- package/lib/test/helpers/fake-logcat.js +80 -0
- package/lib/test/helpers/hub-node-pair.js +227 -0
- package/lib/test/unit/UserService.test.js +8 -0
- package/lib/test/unit/boot-device-sync.spec.js +195 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +15 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +21 -0
- package/lib/test/unit/hub-node-iphone-readiness.spec.js +134 -0
- package/lib/test/unit/hub-node-session-device-logs.spec.js +194 -0
- package/lib/test/unit/hub-node-session-metrics.spec.js +24 -192
- package/lib/test/unit/hub-remote-session-heartbeat.spec.js +8 -1
- package/lib/test/unit/install-scripts.spec.js +98 -0
- package/lib/test/unit/interceptor/CertManager.spec.js +89 -0
- package/lib/test/unit/internal-call.spec.js +99 -0
- package/lib/test/unit/internal-calls-session-record.spec.js +312 -0
- package/lib/test/unit/ios-simulator-preview.spec.js +315 -0
- package/lib/test/unit/is-xenon-running.spec.js +10 -0
- package/lib/test/unit/node-device-log-store.spec.js +102 -0
- package/lib/test/unit/node-device-logs-collector.spec.js +343 -0
- package/lib/test/unit/node-gateway-create-appium-server.spec.js +8 -1
- package/lib/test/unit/node-hub-session-owner.spec.js +8 -1
- package/lib/test/unit/node-session-device-logs-route.spec.js +103 -0
- package/lib/test/unit/run-migrations-database.spec.js +57 -424
- package/lib/test/unit/run-migrations.test.js +74 -674
- package/lib/test/unit/session-device-logs-hub-node.spec.js +340 -0
- package/lib/test/unit/session-device-logs-lifecycle.spec.js +137 -0
- package/lib/test/unit/session-device-logs.spec.js +32 -94
- package/lib/test/unit/session-log-indexes.spec.js +15 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -3
- package/schema.json +1 -1
- package/lib/public/assets/DeviceMosaicView-DqODzkyo.js +0 -2
- package/lib/public/assets/device-explorer-BgGZ8AvV.js +0 -282
|
@@ -60,9 +60,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
60
60
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
61
61
|
};
|
|
62
62
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
63
|
-
exports.StreamRestartRefused = exports.IOS_IDLE_STOP_MS = void 0;
|
|
63
|
+
exports.SimulatorPreviewNeedsTest = exports.SIMULATOR_PREVIEW_NEEDS_TEST = exports.StreamRestartRefused = exports.IOS_IDLE_STOP_MS = void 0;
|
|
64
64
|
exports.shouldStopIdleIosStream = shouldStopIdleIosStream;
|
|
65
65
|
exports.heldByAppiumSession = heldByAppiumSession;
|
|
66
|
+
exports.isIosSimulator = isIosSimulator;
|
|
67
|
+
exports.simulatorPreviewRefusal = simulatorPreviewRefusal;
|
|
66
68
|
const typedi_1 = require("typedi");
|
|
67
69
|
const ProcessRegistry_1 = require("../../services/ProcessRegistry");
|
|
68
70
|
const child_process_1 = require("child_process");
|
|
@@ -82,6 +84,7 @@ const iosStreamDiagnostics_1 = require("./iosStreamDiagnostics");
|
|
|
82
84
|
const tunnelProcess_1 = require("./tunnelProcess");
|
|
83
85
|
const IOSTunnels_1 = require("./IOSTunnels");
|
|
84
86
|
const goIosBinary_1 = require("./goIosBinary");
|
|
87
|
+
const appiumUmbrella_1 = require("../../sessions/appiumUmbrella");
|
|
85
88
|
const device_service_1 = require("../../data-service/device-service");
|
|
86
89
|
const manualLock_1 = require("../../services/recording/manualLock");
|
|
87
90
|
const recording_store_1 = require("../../services/recording/recording-store");
|
|
@@ -113,9 +116,39 @@ function heldByAppiumSession(device) {
|
|
|
113
116
|
class StreamRestartRefused extends Error {
|
|
114
117
|
}
|
|
115
118
|
exports.StreamRestartRefused = StreamRestartRefused;
|
|
119
|
+
/** Why a simulator shows no live preview: for testers, on the device page and its tile. */
|
|
120
|
+
exports.SIMULATOR_PREVIEW_NEEDS_TEST = "A simulator's screen shows here only while a test runs on it.";
|
|
121
|
+
/** A simulator's preview was asked for while no test runs on it. */
|
|
122
|
+
class SimulatorPreviewNeedsTest extends Error {
|
|
123
|
+
constructor() {
|
|
124
|
+
super(exports.SIMULATOR_PREVIEW_NEEDS_TEST);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
exports.SimulatorPreviewNeedsTest = SimulatorPreviewNeedsTest;
|
|
128
|
+
/**
|
|
129
|
+
* An iOS or tvOS simulator's row. `realDevice` is false only there among
|
|
130
|
+
* Apple rows (discovery writes it, the store reads a missing one as true); an
|
|
131
|
+
* Android emulator's is false too.
|
|
132
|
+
*/
|
|
133
|
+
function isIosSimulator(device) {
|
|
134
|
+
return (device === null || device === void 0 ? void 0 : device.realDevice) === false && (device.platform === 'ios' || device.platform === 'tvos');
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Why this phone can't be previewed now, or undefined when it can: a
|
|
138
|
+
* simulator no Appium session holds.
|
|
139
|
+
*/
|
|
140
|
+
function simulatorPreviewRefusal(device) {
|
|
141
|
+
if (!isIosSimulator(device))
|
|
142
|
+
return undefined;
|
|
143
|
+
return heldByAppiumSession(device) ? undefined : exports.SIMULATOR_PREVIEW_NEEDS_TEST;
|
|
144
|
+
}
|
|
145
|
+
/** How many ended sessions a simulator preview remembers, to refuse a start mid-teardown. */
|
|
146
|
+
const ENDED_SESSIONS_KEPT = 200;
|
|
116
147
|
let IOSStreamService = class IOSStreamService {
|
|
117
148
|
constructor() {
|
|
118
149
|
this.sessions = new Map();
|
|
150
|
+
/** Sessions whose simulator previews have ended (endSimulatorPreviews), newest last. */
|
|
151
|
+
this.endedSimulatorSessions = new Set();
|
|
119
152
|
this.startFlight = new singleFlight_1.SingleFlight();
|
|
120
153
|
this.recoveryCooldowns = new Map(); // Track last recovery attempt time
|
|
121
154
|
this.RECOVERY_COOLDOWN_MS = 30000; // 30s cooldown between recovery attempts
|
|
@@ -411,6 +444,10 @@ let IOSStreamService = class IOSStreamService {
|
|
|
411
444
|
const session = this.sessions.get(udid);
|
|
412
445
|
if (!session || session.status !== 'running')
|
|
413
446
|
return false;
|
|
447
|
+
// A simulator's preview has no process of its own to check or heal: the
|
|
448
|
+
// picture is its session's, and the preview ends with it.
|
|
449
|
+
if (session.simulatorSession)
|
|
450
|
+
return true;
|
|
414
451
|
// 1. Process Check: verify child processes haven't exited
|
|
415
452
|
const isWdaAlive = session.wdaProcess && session.wdaProcess.exitCode === null;
|
|
416
453
|
const isWdaIproxyAlive = session.forwardWDAProcess && session.forwardWDAProcess.exitCode === null;
|
|
@@ -453,6 +490,9 @@ let IOSStreamService = class IOSStreamService {
|
|
|
453
490
|
}
|
|
454
491
|
startStream(udid) {
|
|
455
492
|
return __awaiter(this, void 0, void 0, function* () {
|
|
493
|
+
const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
|
|
494
|
+
if (isIosSimulator(device))
|
|
495
|
+
return this.startSimulatorStream(udid, device);
|
|
456
496
|
// Check if stream is already running - avoid unnecessary restarts
|
|
457
497
|
const existingSession = this.sessions.get(udid);
|
|
458
498
|
if (existingSession && existingSession.status === 'running') {
|
|
@@ -811,6 +851,82 @@ let IOSStreamService = class IOSStreamService {
|
|
|
811
851
|
return this.startFlight.run(udid, performStartup);
|
|
812
852
|
});
|
|
813
853
|
}
|
|
854
|
+
/**
|
|
855
|
+
* A simulator's preview is its running test's picture. The XCUITest
|
|
856
|
+
* driver's WDA serves it on this Mac at the session's mjpegServerPort, so
|
|
857
|
+
* this starts nothing (go-ios, iproxy and runwda reach only an iPhone) and
|
|
858
|
+
* leases no port (the session's are its own). With no test running there
|
|
859
|
+
* is nothing to show: refused in plain words, with no cool-down, since a
|
|
860
|
+
* test may start any moment.
|
|
861
|
+
*
|
|
862
|
+
* Through 2.15 a simulator went through the iPhone's start. With no test it
|
|
863
|
+
* failed, and left its iproxy forwards on the ports it had leased to the
|
|
864
|
+
* simulator: a test started meanwhile was given those ports and its WDA
|
|
865
|
+
* couldn't open them. With a test it read the picture through a forward
|
|
866
|
+
* that carries nothing, so a Live devices tile stayed empty.
|
|
867
|
+
*/
|
|
868
|
+
startSimulatorStream(udid, device) {
|
|
869
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
870
|
+
var _a;
|
|
871
|
+
let sessionId = simulatorPreviewRefusal(device) ? undefined : ((_a = device.session_id) !== null && _a !== void 0 ? _a : undefined);
|
|
872
|
+
// An ended test's row can still name it while its phone is being released.
|
|
873
|
+
if (sessionId && this.endedSimulatorSessions.has(sessionId))
|
|
874
|
+
sessionId = undefined;
|
|
875
|
+
// Only the ports of a session Appium has now: the row's would outlive it.
|
|
876
|
+
const driver = sessionId ? typedi_2.Container.get(appiumUmbrella_1.AppiumUmbrella).driverOptions(sessionId) : undefined;
|
|
877
|
+
const wdaPort = Number(driver === null || driver === void 0 ? void 0 : driver.wdaLocalPort);
|
|
878
|
+
const mjpegPort = Number(driver === null || driver === void 0 ? void 0 : driver.mjpegServerPort);
|
|
879
|
+
const existing = this.sessions.get(udid);
|
|
880
|
+
if (!sessionId || !wdaPort || !mjpegPort) {
|
|
881
|
+
if (existing)
|
|
882
|
+
yield this.stopStream(udid);
|
|
883
|
+
throw new SimulatorPreviewNeedsTest();
|
|
884
|
+
}
|
|
885
|
+
if ((existing === null || existing === void 0 ? void 0 : existing.simulatorSession) === sessionId && existing.status === 'running') {
|
|
886
|
+
return { wdaPort: existing.wdaPort, mjpegPort: existing.mjpegPort };
|
|
887
|
+
}
|
|
888
|
+
// An earlier test's preview, or anything else held for this simulator.
|
|
889
|
+
if (existing)
|
|
890
|
+
yield this.stopStream(udid);
|
|
891
|
+
this.sessions.set(udid, {
|
|
892
|
+
udid,
|
|
893
|
+
wdaProcess: null,
|
|
894
|
+
forwardWDAProcess: null,
|
|
895
|
+
forwardMJPEGProcess: null,
|
|
896
|
+
tunnelPort: null,
|
|
897
|
+
wdaPort,
|
|
898
|
+
mjpegPort,
|
|
899
|
+
status: 'running',
|
|
900
|
+
startedAt: new Date(),
|
|
901
|
+
lastViewerAt: Date.now(),
|
|
902
|
+
viewerCount: 0,
|
|
903
|
+
simulatorSession: sessionId,
|
|
904
|
+
});
|
|
905
|
+
logger_1.default.info(`[${udid}] Simulator preview: the picture of session ${sessionId} (port ${mjpegPort})`);
|
|
906
|
+
return { wdaPort, mjpegPort };
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* End the simulator previews that show this session's picture. Called
|
|
911
|
+
* however the session ends (forgetSessionMemory): the driver's WDA may
|
|
912
|
+
* outlive it, but no test runs any more.
|
|
913
|
+
*/
|
|
914
|
+
endSimulatorPreviews(sessionId) {
|
|
915
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
916
|
+
this.endedSimulatorSessions.add(sessionId);
|
|
917
|
+
if (this.endedSimulatorSessions.size > ENDED_SESSIONS_KEPT) {
|
|
918
|
+
const oldest = this.endedSimulatorSessions.values().next().value;
|
|
919
|
+
if (oldest !== undefined)
|
|
920
|
+
this.endedSimulatorSessions.delete(oldest);
|
|
921
|
+
}
|
|
922
|
+
for (const [udid, session] of this.sessions.entries()) {
|
|
923
|
+
if (session.simulatorSession !== sessionId)
|
|
924
|
+
continue;
|
|
925
|
+
logger_1.default.info(`[${udid}] Simulator preview ends with session ${sessionId}`);
|
|
926
|
+
yield this.stopStream(udid);
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
}
|
|
814
930
|
killStaleProcesses(udid, wdaPort, mjpegPort) {
|
|
815
931
|
return __awaiter(this, void 0, void 0, function* () {
|
|
816
932
|
logger_1.default.info(`Cleaning up stale processes for ${udid} (Ports: ${wdaPort}, ${mjpegPort})`);
|
|
@@ -879,10 +995,16 @@ let IOSStreamService = class IOSStreamService {
|
|
|
879
995
|
*/
|
|
880
996
|
stopStream(udid_1) {
|
|
881
997
|
return __awaiter(this, arguments, void 0, function* (udid, opts = {}) {
|
|
882
|
-
var _a;
|
|
883
998
|
const session = this.sessions.get(udid);
|
|
884
999
|
if (!session)
|
|
885
1000
|
return;
|
|
1001
|
+
// A simulator's preview started nothing and leased no port: the ports and
|
|
1002
|
+
// WDA are its session's. Only the entry goes (and a manual hold, below).
|
|
1003
|
+
if (session.simulatorSession) {
|
|
1004
|
+
this.sessions.delete(udid);
|
|
1005
|
+
yield this.releaseManualHold(udid);
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
886
1008
|
if (opts.forViewer &&
|
|
887
1009
|
session.wdaProcess &&
|
|
888
1010
|
(yield this.appiumSessionMayUse(udid, "the stream's WDA and go-ios tunnel"))) {
|
|
@@ -902,22 +1024,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
902
1024
|
// A stream attached to an Appium session's WDA started none.
|
|
903
1025
|
if (session.tunnelPort != null)
|
|
904
1026
|
yield this.tunnels().stop(udid);
|
|
905
|
-
|
|
906
|
-
// The lock could belong to an Appium automation session (session_id is a real UUID).
|
|
907
|
-
// We should only unblock if it's a manual control lock (session_id starts with 'manual_').
|
|
908
|
-
try {
|
|
909
|
-
const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
|
|
910
|
-
if (device && ((_a = device.session_id) === null || _a === void 0 ? void 0 : _a.startsWith('manual_'))) {
|
|
911
|
-
logger_1.default.info(`Stream Stop: Releasing manual control lock for ${udid}`);
|
|
912
|
-
yield (0, device_service_1.unblockDevice)(udid, device.host);
|
|
913
|
-
}
|
|
914
|
-
else if (device && device.busy) {
|
|
915
|
-
logger_1.default.info(`Stream Stop: Device ${udid} is busy with session ${device.session_id}. NOT releasing lock.`);
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
catch (e) {
|
|
919
|
-
logger_1.default.error(`Failed to check/release lock during stream stop for ${udid}: ${e}`);
|
|
920
|
-
}
|
|
1027
|
+
yield this.releaseManualHold(udid);
|
|
921
1028
|
// Also clean up any orphan processes (belt and suspenders)
|
|
922
1029
|
yield this.cleanupOrphanTunnels(udid);
|
|
923
1030
|
// Release the port leases so the allocator can reuse these ports for other
|
|
@@ -933,6 +1040,28 @@ let IOSStreamService = class IOSStreamService {
|
|
|
933
1040
|
this.sessions.delete(udid);
|
|
934
1041
|
});
|
|
935
1042
|
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Release the phone's manual hold (a preview's, a recording's), never an
|
|
1045
|
+
* Appium session's claim: that session_id is a real id, not `manual_...`.
|
|
1046
|
+
*/
|
|
1047
|
+
releaseManualHold(udid) {
|
|
1048
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1049
|
+
var _a;
|
|
1050
|
+
try {
|
|
1051
|
+
const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
|
|
1052
|
+
if (device && ((_a = device.session_id) === null || _a === void 0 ? void 0 : _a.startsWith('manual_'))) {
|
|
1053
|
+
logger_1.default.info(`Stream Stop: Releasing manual control lock for ${udid}`);
|
|
1054
|
+
yield (0, device_service_1.unblockDevice)(udid, device.host);
|
|
1055
|
+
}
|
|
1056
|
+
else if (device && device.busy) {
|
|
1057
|
+
logger_1.default.info(`Stream Stop: Device ${udid} is busy with session ${device.session_id}. NOT releasing lock.`);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
catch (e) {
|
|
1061
|
+
logger_1.default.error(`Failed to check/release lock during stream stop for ${udid}: ${e}`);
|
|
1062
|
+
}
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
936
1065
|
updateWDASettings(wdaPort) {
|
|
937
1066
|
return __awaiter(this, void 0, void 0, function* () {
|
|
938
1067
|
const urls = [`http://127.0.0.1:${wdaPort}/appium/settings`];
|
package/lib/src/device-utils.js
CHANGED
|
@@ -45,7 +45,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
45
45
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
46
|
};
|
|
47
47
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
-
exports.getDeviceTypeFromApp = void 0;
|
|
48
|
+
exports.BOOT_PROBE = exports.getDeviceTypeFromApp = void 0;
|
|
49
49
|
exports.isAndroid = isAndroid;
|
|
50
50
|
exports.deviceType = deviceType;
|
|
51
51
|
exports.isIOS = isIOS;
|
|
@@ -59,6 +59,8 @@ exports.setUtilizationTime = setUtilizationTime;
|
|
|
59
59
|
exports.getDeviceFiltersFromCapability = getDeviceFiltersFromCapability;
|
|
60
60
|
exports.getRunningSessionsCount = getRunningSessionsCount;
|
|
61
61
|
exports.updateDeviceList = updateDeviceList;
|
|
62
|
+
exports.syncLocalDevices = syncLocalDevices;
|
|
63
|
+
exports.reportDevicesToHub = reportDevicesToHub;
|
|
62
64
|
exports.refreshSimulatorState = refreshSimulatorState;
|
|
63
65
|
exports.setupCronCheckStaleDevices = setupCronCheckStaleDevices;
|
|
64
66
|
exports.removeStaleDevices = removeStaleDevices;
|
|
@@ -275,8 +277,17 @@ function allocateDeviceForSession(capability_1, deviceTimeOutMs_1, deviceQueryIn
|
|
|
275
277
|
// We have a locked device here
|
|
276
278
|
if (device !== null) {
|
|
277
279
|
const lockedDevice = device;
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
+
// Ready for a session: asked only of a phone this server drives. The
|
|
281
|
+
// check is this machine's (an iPhone's WebDriverAgent on 127.0.0.1, and a
|
|
282
|
+
// go-ios stream started to recover it). Another server's phone, a node's
|
|
283
|
+
// on a hub, is checked by that server when it allocates the phone the hub
|
|
284
|
+
// pins. Through 2.15 the hub checked it too: with the node on another
|
|
285
|
+
// machine nothing answered and the session was refused as unhealthy, and
|
|
286
|
+
// with both on one Mac the hub started a WebDriverAgent of its own on the
|
|
287
|
+
// node's iPhone.
|
|
288
|
+
const context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
289
|
+
const ownPhone = (0, localDeviceHosts_1.isOwnDevice)((0, localDeviceHosts_1.localDeviceHosts)(context.pluginArgs, context.port), context.nodeId, lockedDevice);
|
|
290
|
+
if (!lockedDevice.cloud && ownPhone) {
|
|
280
291
|
const platform = lockedDevice.platform.toLowerCase();
|
|
281
292
|
const managers = yield getDeviceManager().deviceInstances();
|
|
282
293
|
const manager = managers.find((m) => {
|
|
@@ -521,6 +532,19 @@ function getRunningSessionsCount() {
|
|
|
521
532
|
});
|
|
522
533
|
}
|
|
523
534
|
function updateDeviceList(local, hubArgument, tlsRejectUnauthorized) {
|
|
535
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
536
|
+
const { devices, gone } = yield syncLocalDevices(local);
|
|
537
|
+
if (hubArgument) {
|
|
538
|
+
yield reportDevicesToHub(hubArgument, devices, gone, tlsRejectUnauthorized);
|
|
539
|
+
}
|
|
540
|
+
return devices;
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* This server's own phones: what discovery reports now is written to the
|
|
545
|
+
* table, and its own rows discovery no longer reports are removed (`gone`).
|
|
546
|
+
*/
|
|
547
|
+
function syncLocalDevices(local) {
|
|
524
548
|
return __awaiter(this, void 0, void 0, function* () {
|
|
525
549
|
const allExistingDevices = yield (0, device_service_1.getAllDevices)();
|
|
526
550
|
const devices = yield getDeviceManager().getDevices(allExistingDevices);
|
|
@@ -538,28 +562,31 @@ function updateDeviceList(local, hubArgument, tlsRejectUnauthorized) {
|
|
|
538
562
|
logger_1.default.info(`Removing ${staleLocalDevices.length} stale devices/simulators no longer discovered on this host.`);
|
|
539
563
|
yield (0, device_service_1.removeDevice)(staleLocalDevices.map((d) => ({ udid: d.udid, host: d.host })));
|
|
540
564
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
565
|
+
return { devices, gone: staleLocalDevices };
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
/** A node tells its hub the phones it has, and the ones it no longer has. */
|
|
569
|
+
function reportDevicesToHub(hubArgument, devices, gone, tlsRejectUnauthorized) {
|
|
570
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
571
|
+
if (yield (0, helpers_1.isXenonRunning)(hubArgument, tlsRejectUnauthorized)) {
|
|
572
|
+
const nodeDevices = new NodeDevices_1.default(hubArgument, {
|
|
573
|
+
tlsRejectUnauthorized,
|
|
574
|
+
hubAccessKey: config_1.config.hubAccessKey,
|
|
575
|
+
hubToken: config_1.config.hubToken,
|
|
576
|
+
});
|
|
577
|
+
try {
|
|
578
|
+
yield nodeDevices.postDevicesToHub(devices, 'add');
|
|
579
|
+
if (gone.length > 0) {
|
|
580
|
+
yield nodeDevices.postDevicesToHub(gone, 'remove');
|
|
556
581
|
}
|
|
557
582
|
}
|
|
558
|
-
|
|
559
|
-
logger_1.default.
|
|
583
|
+
catch (error) {
|
|
584
|
+
logger_1.default.error(`Cannot send device list update. Reason: ${error}`);
|
|
560
585
|
}
|
|
561
586
|
}
|
|
562
|
-
|
|
587
|
+
else {
|
|
588
|
+
logger_1.default.warn(`Not sending device update since hub ${hubArgument} is not running`);
|
|
589
|
+
}
|
|
563
590
|
});
|
|
564
591
|
}
|
|
565
592
|
function refreshSimulatorState(pluginArgs, hostPort) {
|
|
@@ -581,11 +608,21 @@ function setupCronCheckStaleDevices(intervalMs, local, tlsRejectUnauthorized) {
|
|
|
581
608
|
}), intervalMs);
|
|
582
609
|
});
|
|
583
610
|
}
|
|
611
|
+
/**
|
|
612
|
+
* How a start asks another server whether it is alive: once, for at most 5 s.
|
|
613
|
+
* Appium opens its port only once every plugin's updateServer has finished,
|
|
614
|
+
* and the client's own way (four 30 s tries) held it closed for two minutes
|
|
615
|
+
* when a row named a host that drops packets. A live server answers in
|
|
616
|
+
* milliseconds; one that answers slower has its phones dropped and reports
|
|
617
|
+
* them again at its next report.
|
|
618
|
+
*/
|
|
619
|
+
exports.BOOT_PROBE = { timeoutMs: 5000, retry: false };
|
|
584
620
|
/**
|
|
585
621
|
* Remove devices where the host is not alive nor defined.
|
|
586
622
|
* @param local the hosts this server files its own phones under
|
|
623
|
+
* @param probe how each host is asked (the client's own way when not given)
|
|
587
624
|
*/
|
|
588
|
-
function removeStaleDevices(local, tlsRejectUnauthorized) {
|
|
625
|
+
function removeStaleDevices(local, tlsRejectUnauthorized, probe) {
|
|
589
626
|
return __awaiter(this, void 0, void 0, function* () {
|
|
590
627
|
const allDevices = yield (0, device_service_1.getAllDevices)();
|
|
591
628
|
const nodeId = typedi_1.Container.get(PluginContext_1.PluginContext).nodeId;
|
|
@@ -602,13 +639,13 @@ function removeStaleDevices(local, tlsRejectUnauthorized) {
|
|
|
602
639
|
const aliveHosts = (yield Promise.allSettled(nodeHosts.map((host) => __awaiter(this, void 0, void 0, function* () {
|
|
603
640
|
return {
|
|
604
641
|
host: host,
|
|
605
|
-
alive: yield (0, helpers_1.isXenonRunning)(host, tlsRejectUnauthorized),
|
|
642
|
+
alive: yield (0, helpers_1.isXenonRunning)(host, tlsRejectUnauthorized, probe),
|
|
606
643
|
};
|
|
607
644
|
}))));
|
|
608
645
|
const aliveCloudHosts = (yield Promise.allSettled(cloudHosts.map((host) => __awaiter(this, void 0, void 0, function* () {
|
|
609
646
|
return {
|
|
610
647
|
host: host,
|
|
611
|
-
alive: yield (0, helpers_1.isAppiumRunningAt)(host, tlsRejectUnauthorized),
|
|
648
|
+
alive: yield (0, helpers_1.isAppiumRunningAt)(host, tlsRejectUnauthorized, probe),
|
|
612
649
|
};
|
|
613
650
|
}))));
|
|
614
651
|
// summarize alive hosts
|
|
@@ -4,8 +4,10 @@ exports.INTERNAL_CALL_MARKER = exports.INTERNAL_CALL_HEADER = void 0;
|
|
|
4
4
|
exports.internalCallHeaders = internalCallHeaders;
|
|
5
5
|
exports.hasInternalCallSecret = hasInternalCallSecret;
|
|
6
6
|
exports.isInternalCall = isInternalCall;
|
|
7
|
+
exports.isInsideInternalCall = isInsideInternalCall;
|
|
7
8
|
exports.internalCallBaseUrl = internalCallBaseUrl;
|
|
8
9
|
exports.createInternalCallLayer = createInternalCallLayer;
|
|
10
|
+
const async_hooks_1 = require("async_hooks");
|
|
9
11
|
const crypto_1 = require("crypto");
|
|
10
12
|
const appiumBasePath_1 = require("../app/appiumBasePath");
|
|
11
13
|
/**
|
|
@@ -27,11 +29,20 @@ const appiumBasePath_1 = require("../app/appiumBasePath");
|
|
|
27
29
|
* The secret never leaves the process: it is sent only to this server's own
|
|
28
30
|
* address, with any HTTP proxy bypassed, and the gateway removes the header
|
|
29
31
|
* before anything after it sees the request.
|
|
32
|
+
*
|
|
33
|
+
* Such a call is never one of the test's commands. The rest of its request
|
|
34
|
+
* runs in an async context the plugin can read (`isInsideInternalCall`), since
|
|
35
|
+
* the plugin's `handle`, deep inside Appium's route, is never given the
|
|
36
|
+
* request. Through 2.16 it couldn't tell, so the dashboard recorded the call
|
|
37
|
+
* as the session's own: a performance recording's stop the driver refused at
|
|
38
|
+
* the end of an iPhone session (through 2.15, of every simulator session)
|
|
39
|
+
* failed a session whose commands had all passed.
|
|
30
40
|
*/
|
|
31
41
|
exports.INTERNAL_CALL_HEADER = 'x-xenon-internal';
|
|
32
42
|
exports.INTERNAL_CALL_MARKER = '/wd-internal';
|
|
33
43
|
const SECRET = Buffer.from((0, crypto_1.randomBytes)(32).toString('base64url'));
|
|
34
44
|
const INTERNAL = Symbol('xenon.internalCall');
|
|
45
|
+
const internalCalls = new async_hooks_1.AsyncLocalStorage();
|
|
35
46
|
/** The headers a loopback call to this process's own server carries. */
|
|
36
47
|
function internalCallHeaders() {
|
|
37
48
|
return { [exports.INTERNAL_CALL_HEADER]: SECRET.toString() };
|
|
@@ -48,6 +59,19 @@ function hasInternalCallSecret(headers) {
|
|
|
48
59
|
function isInternalCall(req) {
|
|
49
60
|
return !!req && req[INTERNAL] === true;
|
|
50
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Whether this code runs for a request the internal-call layer accepted: the
|
|
64
|
+
* plugin's `handle` and the driver's command under it, which never see the
|
|
65
|
+
* request itself.
|
|
66
|
+
*
|
|
67
|
+
* Read it where a command enters the plugin (CommandInterceptor.handle). It
|
|
68
|
+
* also holds in whatever the driver starts during the call and runs later (a
|
|
69
|
+
* timer, a socket's callback): base-driver's new-command timer, restarted at
|
|
70
|
+
* the call's end, fires inside it, and so does an idle session's shutdown.
|
|
71
|
+
*/
|
|
72
|
+
function isInsideInternalCall() {
|
|
73
|
+
return internalCalls.getStore() === true;
|
|
74
|
+
}
|
|
51
75
|
/**
|
|
52
76
|
* Where a local session's loopback calls go: this server, under its base
|
|
53
77
|
* path, marked `/wd-internal`. A wildcard bind address is reached on 127.0.0.1.
|
|
@@ -80,11 +104,15 @@ function createInternalCallLayer(basePath) {
|
|
|
80
104
|
req.originalUrl = req.url;
|
|
81
105
|
return true;
|
|
82
106
|
}
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
107
|
+
// Every request this layer leaves alone goes on through the one call to
|
|
108
|
+
// next() below: Appium's 404 carries a stack trace, and a second call site
|
|
109
|
+
// would make a refused /wd-internal request's answer differ from any other
|
|
110
|
+
// unknown route's by that one frame. An accepted call goes on inside its
|
|
111
|
+
// context, down to the plugin's handle (isInsideInternalCall).
|
|
86
112
|
return function xenonInternalCalls(req, _res, next) {
|
|
87
|
-
accept(req)
|
|
88
|
-
|
|
113
|
+
if (accept(req))
|
|
114
|
+
internalCalls.run(true, next);
|
|
115
|
+
else
|
|
116
|
+
next();
|
|
89
117
|
};
|
|
90
118
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OlderNodes = exports.OLDER_NODE_RECHECK_MS = void 0;
|
|
4
|
+
exports.readNodeReply = readNodeReply;
|
|
5
|
+
/**
|
|
6
|
+
* The node's answer at a route whose answers carry `header`. `answerOf` reads
|
|
7
|
+
* a 200's `value`, or returns null when it isn't one.
|
|
8
|
+
*/
|
|
9
|
+
function readNodeReply(header, status, headers, data, answerOf) {
|
|
10
|
+
if (!(headers === null || headers === void 0 ? void 0 : headers[header])) {
|
|
11
|
+
// An older node answers its login's 401, an unknown route's 404, or a
|
|
12
|
+
// catch-all's 2xx. Anything else without the header (a proxy's 502 in
|
|
13
|
+
// front of a node) is an outage: ask again, never give the node up.
|
|
14
|
+
if (status === 401 || status === 404 || (status >= 200 && status < 300)) {
|
|
15
|
+
return { kind: 'unsupported', status };
|
|
16
|
+
}
|
|
17
|
+
return { kind: 'unavailable', reason: `answered ${status}` };
|
|
18
|
+
}
|
|
19
|
+
if (status === 404)
|
|
20
|
+
return { kind: 'refused' };
|
|
21
|
+
const answer = status === 200 ? answerOf(data === null || data === void 0 ? void 0 : data.value) : null;
|
|
22
|
+
if (answer !== null)
|
|
23
|
+
return { kind: 'answer', answer };
|
|
24
|
+
return { kind: 'unavailable', reason: `answered ${status}` };
|
|
25
|
+
}
|
|
26
|
+
/** How long a hub leaves a node without a route alone before asking it again. */
|
|
27
|
+
exports.OLDER_NODE_RECHECK_MS = 10 * 60000;
|
|
28
|
+
/**
|
|
29
|
+
* Hub side: which nodes lack a route (an older Xenon). Their sessions go
|
|
30
|
+
* without what the route serves; the hub says so once per node, and asks
|
|
31
|
+
* again after OLDER_NODE_RECHECK_MS, so a node upgraded in place is picked
|
|
32
|
+
* up without restarting the hub.
|
|
33
|
+
*/
|
|
34
|
+
class OlderNodes {
|
|
35
|
+
constructor() {
|
|
36
|
+
this.now = () => Date.now();
|
|
37
|
+
this.unsupportedUntil = new Map();
|
|
38
|
+
this.warned = new Set();
|
|
39
|
+
}
|
|
40
|
+
shouldAsk(origin) {
|
|
41
|
+
const until = this.unsupportedUntil.get(origin);
|
|
42
|
+
if (until === undefined)
|
|
43
|
+
return true;
|
|
44
|
+
if (until > this.now())
|
|
45
|
+
return false;
|
|
46
|
+
this.unsupportedUntil.delete(origin);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
unsupported(origin, status) {
|
|
50
|
+
this.unsupportedUntil.set(origin, this.now() + exports.OLDER_NODE_RECHECK_MS);
|
|
51
|
+
if (this.warned.has(origin))
|
|
52
|
+
return;
|
|
53
|
+
this.warned.add(origin);
|
|
54
|
+
this.logger.warn(this.warning(origin, status));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
exports.OlderNodes = OlderNodes;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.nodeSessionDeviceLogsHandler = nodeSessionDeviceLogsHandler;
|
|
7
|
+
exports.registerNodeSessionDeviceLogs = registerNodeSessionDeviceLogs;
|
|
8
|
+
const typedi_1 = require("typedi");
|
|
9
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
10
|
+
const config_1 = require("../config");
|
|
11
|
+
const commandAuth_1 = require("../middleware/commandAuth");
|
|
12
|
+
const nodeDeviceLogs_1 = require("../services/logcat/nodeDeviceLogs");
|
|
13
|
+
const NodeDeviceLogStore_1 = require("../services/logcat/NodeDeviceLogStore");
|
|
14
|
+
const hubSessionToken_1 = require("./hubSessionToken");
|
|
15
|
+
const nodeSessionMetrics_1 = require("./nodeSessionMetrics");
|
|
16
|
+
const nodeSessionStatus_1 = require("./nodeSessionStatus");
|
|
17
|
+
function nodeSessionDeviceLogsHandler(deps) {
|
|
18
|
+
return (req, res) => {
|
|
19
|
+
var _a, _b;
|
|
20
|
+
res.setHeader(nodeDeviceLogs_1.NODE_DEVICE_LOGS_HEADER, '1');
|
|
21
|
+
const sessionId = String((_b = (_a = req.params) === null || _a === void 0 ? void 0 : _a.sessionId) !== null && _b !== void 0 ? _b : '');
|
|
22
|
+
(0, nodeSessionStatus_1.answerForHub)(deps, req, res, sessionId, 'Session device logs', () => { var _a; return res.status(200).json({ value: deps.read(sessionId, (0, nodeSessionMetrics_1.parseAfter)((_a = req.query) === null || _a === void 0 ? void 0 : _a.after)) }); });
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** A node's route, ahead of its login, like the session-status route. A hub or standalone server has none. */
|
|
26
|
+
function registerNodeSessionDeviceLogs(router, pluginArgs) {
|
|
27
|
+
if (pluginArgs.hub === undefined)
|
|
28
|
+
return;
|
|
29
|
+
router.get(nodeDeviceLogs_1.NODE_DEVICE_LOGS_ROUTE, nodeSessionDeviceLogsHandler({
|
|
30
|
+
hubTokens: new hubSessionToken_1.HubSessionTokenVerifier(pluginArgs.hub),
|
|
31
|
+
enforced: () => (0, commandAuth_1.commandAuthEnabled)() && config_1.config.authDisabled !== true,
|
|
32
|
+
read: (sessionId, after) => typedi_1.Container.get(NodeDeviceLogStore_1.NodeDeviceLogStore).read(sessionId, after),
|
|
33
|
+
logger: logger_1.default.scope('NodeSessionDeviceLogs'),
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
@@ -168,10 +168,15 @@ function createHubRouting(deps) {
|
|
|
168
168
|
yield deleteRemote(req, res, sessionId, location);
|
|
169
169
|
return;
|
|
170
170
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
171
|
+
// Xenon's own call (internalCall.ts) is never one of the test's
|
|
172
|
+
// commands: it isn't the session's activity, nor in its record.
|
|
173
|
+
const own = (0, internalCall_1.isInternalCall)(req);
|
|
174
|
+
if (!own) {
|
|
175
|
+
yield deps
|
|
176
|
+
.touch(sessionId)
|
|
177
|
+
.catch((error) => logger.warn(`Could not record activity for session ${sessionId}: ${(0, commandAuth_1.summarize)(error)}`));
|
|
178
|
+
}
|
|
179
|
+
const hooks = location.session && !location.cloud && !own ? deps.dashboard : undefined;
|
|
175
180
|
const command = hooks
|
|
176
181
|
? deps.commandName(`${deps.basePath}/session/${sessionId}${rest.split('?')[0]}`, req.method)
|
|
177
182
|
: undefined;
|
package/lib/src/helpers/index.js
CHANGED
|
@@ -176,15 +176,18 @@ function stripAppiumPrefixes(caps) {
|
|
|
176
176
|
}
|
|
177
177
|
return strippedCaps;
|
|
178
178
|
}
|
|
179
|
-
function
|
|
180
|
-
return
|
|
179
|
+
function probeConfig(probe) {
|
|
180
|
+
return Object.assign(Object.assign({}, (probe.timeoutMs !== undefined ? { timeout: probe.timeoutMs } : {})), (probe.retry === false ? { retry: false } : {}));
|
|
181
|
+
}
|
|
182
|
+
function isXenonRunning(host_1, tlsRejectUnauthorized_1) {
|
|
183
|
+
return __awaiter(this, arguments, void 0, function* (host, tlsRejectUnauthorized, probe = {}) {
|
|
181
184
|
try {
|
|
182
185
|
const client = InternalHttpClient_1.InternalHttpClient.getClient(tlsRejectUnauthorized);
|
|
183
186
|
// /health is the one /xenon/api route with no login. The hub probes its
|
|
184
187
|
// nodes, and a node its hub, without credentials, so a login-only route
|
|
185
188
|
// (/status) called a server with auth on dead: the hub pruned its node's
|
|
186
189
|
// phones, and the node never registered with its hub.
|
|
187
|
-
yield client.get(`${host}/xenon/api/health
|
|
190
|
+
yield client.get(`${host}/xenon/api/health`, probeConfig(probe));
|
|
188
191
|
return true;
|
|
189
192
|
}
|
|
190
193
|
catch (error) {
|
|
@@ -193,11 +196,11 @@ function isXenonRunning(host, tlsRejectUnauthorized) {
|
|
|
193
196
|
}
|
|
194
197
|
});
|
|
195
198
|
}
|
|
196
|
-
function isAppiumRunningAt(
|
|
197
|
-
return __awaiter(this,
|
|
199
|
+
function isAppiumRunningAt(url_1, tlsRejectUnauthorized_1) {
|
|
200
|
+
return __awaiter(this, arguments, void 0, function* (url, tlsRejectUnauthorized, probe = {}) {
|
|
198
201
|
try {
|
|
199
202
|
const client = InternalHttpClient_1.InternalHttpClient.getClient(tlsRejectUnauthorized);
|
|
200
|
-
yield client.get(`${url}/status
|
|
203
|
+
yield client.get(`${url}/status`, probeConfig(probe));
|
|
201
204
|
return true;
|
|
202
205
|
}
|
|
203
206
|
catch (error) {
|
|
@@ -70,6 +70,7 @@ const waitFor_1 = require("../services/autowait/waitFor");
|
|
|
70
70
|
const SelfHealingSwitch_1 = require("../services/settings/SelfHealingSwitch");
|
|
71
71
|
const xenonScripts_1 = require("./xenonScripts");
|
|
72
72
|
const healReport_1 = require("../gateway/healReport");
|
|
73
|
+
const internalCall_1 = require("../gateway/internalCall");
|
|
73
74
|
const W3C_ELEMENT_KEY = 'element-6066-11e4-a52e-4f735466cecf';
|
|
74
75
|
/**
|
|
75
76
|
* The commands the interceptor answers itself for an element Xenon found in a
|
|
@@ -116,6 +117,15 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
116
117
|
const IGNORED_COMMANDS = ['getScreenshot', 'stopRecordingScreen', 'startRecordingScreen'];
|
|
117
118
|
if (IGNORED_COMMANDS.includes(commandName))
|
|
118
119
|
return yield next();
|
|
120
|
+
// Xenon's own call to the session (a LocalSession's loopback, when its
|
|
121
|
+
// in-process call failed; gateway/internalCall.ts) goes straight to the
|
|
122
|
+
// driver. It isn't one of the test's commands: it isn't recorded with
|
|
123
|
+
// them, so it can't decide the session's status, and it doesn't reset
|
|
124
|
+
// Xenon's idle clock. Through 2.16 a performance recording's stop the
|
|
125
|
+
// driver refused at the end was recorded as a failed `execute`, and the
|
|
126
|
+
// session ended failed.
|
|
127
|
+
if ((0, internalCall_1.isInsideInternalCall)())
|
|
128
|
+
return yield next();
|
|
119
129
|
const sessionId = driver.sessionId || args[args.length - 1];
|
|
120
130
|
const tracingService = typedi_1.Container.get(TracingService_1.TracingService);
|
|
121
131
|
const spanId = `${sessionId}:${commandName}`;
|
package/lib/src/plugin.js
CHANGED
|
@@ -68,6 +68,7 @@ const appiumUmbrella_1 = require("./sessions/appiumUmbrella");
|
|
|
68
68
|
const LiveSessionOwners_1 = require("./services/device-access/LiveSessionOwners");
|
|
69
69
|
const sessionMemory_1 = require("./sessions/sessionMemory");
|
|
70
70
|
const SessionMetricsService_1 = require("./services/metrics/SessionMetricsService");
|
|
71
|
+
const SessionDeviceLogs_1 = require("./services/logcat/SessionDeviceLogs");
|
|
71
72
|
const PhoneNetworkRestore_1 = require("./services/network/PhoneNetworkRestore");
|
|
72
73
|
const config_1 = require("./config");
|
|
73
74
|
const SessionManager_1 = require("./sessions/SessionManager");
|
|
@@ -120,8 +121,10 @@ class XenonPlugin extends base_plugin_1.default {
|
|
|
120
121
|
reason: (0, shutdownReason_1.unexpectedShutdownReason)(cause),
|
|
121
122
|
});
|
|
122
123
|
}
|
|
123
|
-
if (sessionId)
|
|
124
|
+
if (sessionId) {
|
|
124
125
|
yield typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService).stop(sessionId);
|
|
126
|
+
yield typedi_1.Container.get(SessionDeviceLogs_1.SessionDeviceLogs).stop(sessionId);
|
|
127
|
+
}
|
|
125
128
|
// Appium's new-command timeout ends a session here, not in deleteSession:
|
|
126
129
|
// the phone's network (profile, interceptor proxy) and the capture are
|
|
127
130
|
// put back before the phone is released.
|