@xenon-device-management/xenon 2.9.1 → 2.9.2
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/lib/package.json +1 -1
- package/lib/public/assets/{AnnotationOverlay-Dm_URoOL.js → AnnotationOverlay-B87n4pUu.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-BdGOuXDm.js → ApiKeyGate-CNZJH6Cs.js} +1 -1
- package/lib/public/assets/{BugReportButton-BuWGEjgw.js → BugReportButton-DQ-R87ZV.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-Pu3rJ99D.js → DeviceMosaicView-dX3e7O-w.js} +1 -1
- package/lib/public/assets/{EmptyState-syyHElMd.js → EmptyState-D3fG2GHY.js} +1 -1
- package/lib/public/assets/{FieldGroup-BZR5ZBVv.js → FieldGroup-CUmbJZxI.js} +1 -1
- package/lib/public/assets/{FilterMenu-CsgtrmPb.js → FilterMenu-Dm2c-UCk.js} +1 -1
- package/lib/public/assets/{Menu-DP8XOm1A.js → Menu-Dl4uXMkH.js} +1 -1
- package/lib/public/assets/{Modal-Dtd-8uI2.js → Modal-DNmn2RDs.js} +1 -1
- package/lib/public/assets/{RecordingPage-CdAtAmHj.js → RecordingPage-Dn3By34o.js} +1 -1
- package/lib/public/assets/{RecordingsPage-Br3CKxTe.js → RecordingsPage-lCUIM4pm.js} +1 -1
- package/lib/public/assets/{SegmentedControl-BOaNmUL-.js → SegmentedControl-BdA_mHiZ.js} +1 -1
- package/lib/public/assets/{SettingCard-CQuK9ZZX.js → SettingCard-Dx3WhK7w.js} +1 -1
- package/lib/public/assets/{Table-CswCefnT.js → Table-D_2lhvRM.js} +1 -1
- package/lib/public/assets/{activity-6fhH3u4C.js → activity-cNHHOKx5.js} +1 -1
- package/lib/public/assets/{ai-settings-CU24L3D4.js → ai-settings-t6RMrDkg.js} +1 -1
- package/lib/public/assets/{api-keys-DC3BZDaG.js → api-keys-CK_qyQvW.js} +1 -1
- package/lib/public/assets/{apps-Ce5Z-wNc.js → apps-DCiI3UOb.js} +1 -1
- package/lib/public/assets/{arrow-left-DZJqYn7r.js → arrow-left-CnYs8HVw.js} +1 -1
- package/lib/public/assets/{arrow-right-nIvK8CyN.js → arrow-right-Cc9qm6UG.js} +1 -1
- package/lib/public/assets/{arrow-up-right-BfugJj3f.js → arrow-up-right-e7yBOb-c.js} +1 -1
- package/lib/public/assets/{auth-shell-B1Cz75Vq.js → auth-shell-DKrtcIjG.js} +2 -2
- package/lib/public/assets/{builds-page-5mlJGjIk.js → builds-page-CRd5oEz6.js} +1 -1
- package/lib/public/assets/{button-D_W88sn-.js → button-C0caCjJB.js} +1 -1
- package/lib/public/assets/{calendar-Dc6pWZS9.js → calendar-BqOOkln_.js} +1 -1
- package/lib/public/assets/{check-BbvtnP8m.js → check-DYSXRG79.js} +1 -1
- package/lib/public/assets/{chevron-left-DQDpR9ne.js → chevron-left-BSG6mcpo.js} +1 -1
- package/lib/public/assets/{chevron-right-CgBUaRCA.js → chevron-right-hCNu0CA-.js} +1 -1
- package/lib/public/assets/{circle-check-BWjEKvA4.js → circle-check-DnQ263fa.js} +1 -1
- package/lib/public/assets/{circle-x-BLUdeRhT.js → circle-x-BaRKZyz9.js} +1 -1
- package/lib/public/assets/{clock-PAhKG1jT.js → clock-XP3iBAhg.js} +1 -1
- package/lib/public/assets/{copy-2CHdAPBf.js → copy-D8d9UzWE.js} +1 -1
- package/lib/public/assets/{device-explorer-EyAhEL_S.js → device-explorer-pdM3L9TC.js} +1 -1
- package/lib/public/assets/{download-B1vKGhGP.js → download-SaN9-umX.js} +1 -1
- package/lib/public/assets/{forgot-password-NSrrBA5N.js → forgot-password-DfUhEZW5.js} +1 -1
- package/lib/public/assets/{funnel-C9mzkCSI.js → funnel-OefHE-54.js} +1 -1
- package/lib/public/assets/{index-BxaH84UO.js → index-CzaIyL9t.js} +2 -2
- package/lib/public/assets/{index-BEk1D5-B.js → index-IefGCFQQ.js} +1 -1
- package/lib/public/assets/{input-cA_lrNIw.js → input-BqFzAQT3.js} +1 -1
- package/lib/public/assets/{keycodes-DZLCZ-uv.js → keycodes-DDV2OlpQ.js} +1 -1
- package/lib/public/assets/{layers-BuXMIJz3.js → layers-a3GNz2DA.js} +1 -1
- package/lib/public/assets/{list-checks-B8q68tPG.js → list-checks-DMrMAFDt.js} +1 -1
- package/lib/public/assets/{lock-ChoclF8X.js → lock-Eye1CG8p.js} +1 -1
- package/lib/public/assets/{login-63DiXJ0M.js → login-Co1IXuen.js} +1 -1
- package/lib/public/assets/{maintenance-settings-A47Y-PYy.js → maintenance-settings-D1fw6iqm.js} +1 -1
- package/lib/public/assets/{monitor-BYPF43Ds.js → monitor-U5Y-PxW5.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-Pcl7inmZ.js → mouse-pointer-2-CDAwphUm.js} +1 -1
- package/lib/public/assets/{network-ChaLfnWe.js → network-BP_U4iDZ.js} +1 -1
- package/lib/public/assets/{overview-bj_vktQG.js → overview-CsPyPv10.js} +1 -1
- package/lib/public/assets/{page-header-bv2AQSMG.js → page-header-ZdzNEJp9.js} +1 -1
- package/lib/public/assets/{plus-u0x2Y5uW.js → plus-1k0sxXyU.js} +1 -1
- package/lib/public/assets/{profile-page-Cvmef2iY.js → profile-page-B_GWKRy3.js} +1 -1
- package/lib/public/assets/{reset-password-DqStVhBo.js → reset-password-DizCTkdw.js} +1 -1
- package/lib/public/assets/{runbook-page-UK0C95QF.js → runbook-page-DWzRiOLZ.js} +1 -1
- package/lib/public/assets/{select-CQ6O804R.js → select-CiRW6v17.js} +1 -1
- package/lib/public/assets/{selector-detail-page-D-PjU6mC.js → selector-detail-page-pA913STP.js} +1 -1
- package/lib/public/assets/{selector-health-CVm0cTOU.js → selector-health-CHLX8KqY.js} +1 -1
- package/lib/public/assets/{selector-health-page-BdibBs8g.js → selector-health-page-vVkg8Mii.js} +1 -1
- package/lib/public/assets/{session-detail-page-BijicGOF.js → session-detail-page-BCD49zDi.js} +1 -1
- package/lib/public/assets/{settings-Cva7V4n-.js → settings-DhwZoWv7.js} +1 -1
- package/lib/public/assets/{stat-tile-BdIoEoou.js → stat-tile-BaE76cnK.js} +1 -1
- package/lib/public/assets/{tablet-BPtlTsKn.js → tablet-CPJV3dBq.js} +1 -1
- package/lib/public/assets/{teams-D4r1UMME.js → teams-D0IBa2Ov.js} +1 -1
- package/lib/public/assets/{trash-2-CnysPtdR.js → trash-2-u8MlXUBD.js} +1 -1
- package/lib/public/assets/{upload-Uyj4-Cyq.js → upload-Cq8L3kpw.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-Du-ykGHP.js → use-password-reset-mode-Dbr2HEfw.js} +1 -1
- package/lib/public/assets/{users-ttkwKBos.js → users-CZqCE511.js} +1 -1
- package/lib/public/assets/{users-Cs00GNmy.js → users-DxLHYmFm.js} +1 -1
- package/lib/public/assets/{video-off-BYa4qD8N.js → video-off-mh1EEzP9.js} +1 -1
- package/lib/public/assets/{webhook-settings-DuEnPlet.js → webhook-settings-BYuOib-Q.js} +1 -1
- package/lib/public/assets/{zap-CxHn6Vwu.js → zap-BOpUur0M.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/device-managers/ios/IOSDiscoveryService.js +6 -65
- package/lib/src/device-managers/ios/IOSStreamService.js +7 -66
- package/lib/src/device-managers/ios/WDAClient.js +65 -86
- package/lib/test/unit/ios-discovery-same-host.spec.js +0 -1
- package/lib/test/unit/ios-no-network-fallback.spec.js +219 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -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 __metadata = (this && this.__metadata) || function (k, v) {
|
|
42
9
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
43
10
|
};
|
|
@@ -60,10 +27,8 @@ const node_simctl_1 = __importDefault(require("node-simctl"));
|
|
|
60
27
|
const lodash_1 = require("lodash");
|
|
61
28
|
const appium_ios_device_1 = require("appium-ios-device");
|
|
62
29
|
const appleIdentity_1 = require("./appleIdentity");
|
|
63
|
-
const goIosBinary_1 = require("./goIosBinary");
|
|
64
30
|
const deviceIdentity_1 = require("../deviceIdentity");
|
|
65
31
|
const logger_1 = __importDefault(require("../../logger"));
|
|
66
|
-
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
67
32
|
const device_utils_1 = require("../../device-utils");
|
|
68
33
|
const device_store_1 = require("../../data-service/device-store");
|
|
69
34
|
const PluginContext_1 = require("../../PluginContext");
|
|
@@ -160,8 +125,7 @@ let IOSDiscoveryService = class IOSDiscoveryService {
|
|
|
160
125
|
try {
|
|
161
126
|
const existingDevice = existingDeviceDetails.find((device) => device.udid === udid && device.host === ownHost);
|
|
162
127
|
if (existingDevice) {
|
|
163
|
-
|
|
164
|
-
return Object.assign(Object.assign({}, existingDevice), { ip: networkIp || (0, networkAddresses_1.sanitizeDeviceNetworkIp)(existingDevice.ip) || '' });
|
|
128
|
+
return Object.assign(Object.assign({}, existingDevice), { ip: (0, networkAddresses_1.sanitizeDeviceNetworkIp)(existingDevice.ip) || '' });
|
|
165
129
|
}
|
|
166
130
|
return yield this.getDeviceInfo(udid);
|
|
167
131
|
}
|
|
@@ -192,7 +156,6 @@ let IOSDiscoveryService = class IOSDiscoveryService {
|
|
|
192
156
|
const totalUtilizationTimeMilliSec = yield (0, device_utils_1.getUtilizationTime)(udid);
|
|
193
157
|
let sdk = 'Unknown';
|
|
194
158
|
let name = 'iPhone';
|
|
195
|
-
const ipAddress = yield this.fetchRealDeviceNetworkIp(udid);
|
|
196
159
|
try {
|
|
197
160
|
[sdk, name] = yield Promise.all([appium_ios_device_1.utilities.getOSVersion(udid), appium_ios_device_1.utilities.getDeviceName(udid)]);
|
|
198
161
|
}
|
|
@@ -211,7 +174,11 @@ let IOSDiscoveryService = class IOSDiscoveryService {
|
|
|
211
174
|
mjpegServerPort,
|
|
212
175
|
udid,
|
|
213
176
|
sdk,
|
|
214
|
-
name,
|
|
177
|
+
name,
|
|
178
|
+
// go-ios can't tell a phone's network address (its lockdown values
|
|
179
|
+
// carry none), and Xenon reaches a phone only through its forwarded
|
|
180
|
+
// ports, so nothing asks for it.
|
|
181
|
+
ip: '', busy: false, realDevice: true, deviceType: 'real', cpuArchitecture: 'arm64', platform: (name.toLowerCase().includes('tv') ? 'tvos' : 'ios'), host: host, totalUtilizationTimeMilliSec, sessionStartTime: 0, state: (storeDevice === null || storeDevice === void 0 ? void 0 : storeDevice.state) || 'Unknown', userBlocked: (storeDevice === null || storeDevice === void 0 ? void 0 : storeDevice.userBlocked) || false }, (storeDevice || {})), (0, deviceIdentity_1.nonNullIdentity)(identity));
|
|
215
182
|
});
|
|
216
183
|
}
|
|
217
184
|
getSimulators() {
|
|
@@ -280,32 +247,6 @@ let IOSDiscoveryService = class IOSDiscoveryService {
|
|
|
280
247
|
resolveNodeLanIp() {
|
|
281
248
|
return (0, networkAddresses_1.resolveAdvertisedBindHost)(this.pluginArgs.bindHostOrIp);
|
|
282
249
|
}
|
|
283
|
-
fetchRealDeviceNetworkIp(udid) {
|
|
284
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
285
|
-
return (0, networkAddresses_1.sanitizeDeviceNetworkIp)(yield this.fetchIpViaGoIos(udid));
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
fetchIpViaGoIos(udid) {
|
|
289
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
290
|
-
const goIOSPath = (0, goIosBinary_1.goIosBinaryPath)();
|
|
291
|
-
if (!fs_extra_1.default.existsSync(goIOSPath))
|
|
292
|
-
return '';
|
|
293
|
-
const { exec } = yield Promise.resolve().then(() => __importStar(require('child_process')));
|
|
294
|
-
const { promisify } = yield Promise.resolve().then(() => __importStar(require('util')));
|
|
295
|
-
const execPromise = promisify(exec);
|
|
296
|
-
try {
|
|
297
|
-
const { stdout } = yield execPromise(`"${goIOSPath}" info --udid ${udid}`, {
|
|
298
|
-
env: Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' }),
|
|
299
|
-
});
|
|
300
|
-
const info = JSON.parse(stdout);
|
|
301
|
-
return info.IPAddress || '';
|
|
302
|
-
}
|
|
303
|
-
catch (e) {
|
|
304
|
-
logger_1.default.debug(`Failed to fetch IP via go-ios for ${udid}: ${e}`);
|
|
305
|
-
return '';
|
|
306
|
-
}
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
250
|
trackIOSDevices() {
|
|
310
251
|
if (this.trackingInitialized)
|
|
311
252
|
return;
|
|
@@ -401,9 +401,10 @@ let IOSStreamService = class IOSStreamService {
|
|
|
401
401
|
});
|
|
402
402
|
}
|
|
403
403
|
/**
|
|
404
|
-
* Comprehensive process-level and endpoint-level health check
|
|
405
|
-
*
|
|
406
|
-
*
|
|
404
|
+
* Comprehensive process-level and endpoint-level health check: WDA and its
|
|
405
|
+
* forwarders are running, and WDA answers on the stream's own port. Only
|
|
406
|
+
* that port: WDA names no phone, so nothing that answers elsewhere (a
|
|
407
|
+
* simulator's address is this Mac's own) is known to be this phone's.
|
|
407
408
|
*/
|
|
408
409
|
isStreamResponsive(udid) {
|
|
409
410
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -419,78 +420,18 @@ let IOSStreamService = class IOSStreamService {
|
|
|
419
420
|
return false;
|
|
420
421
|
}
|
|
421
422
|
if (!isWdaIproxyAlive || !isMjpegIproxyAlive) {
|
|
422
|
-
logger_1.default.warn(`🛡️ [${udid}] [Watchdog]
|
|
423
|
-
|
|
424
|
-
if (device && device.ip) {
|
|
425
|
-
// Double check if WDA is alive via network IP
|
|
426
|
-
const isWdaAccessibleViaNetwork = yield this.isWDARunningOnHost(device.ip, 8100);
|
|
427
|
-
if (isWdaAccessibleViaNetwork) {
|
|
428
|
-
logger_1.default.info(`🛡️ [${udid}] [Watchdog] WDA is alive on network ${device.ip}. Restarting tunnels...`);
|
|
429
|
-
yield this.restartTunnelsOnly(session);
|
|
430
|
-
return true; // We healed it!
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
return false; // Cannot heal without network access or if WDA is also dead
|
|
423
|
+
logger_1.default.warn(`🛡️ [${udid}] [Watchdog] Port forwarder processes are dead.`);
|
|
424
|
+
return false;
|
|
434
425
|
}
|
|
435
426
|
// 2. Network Check: verify endpoint is responding via tunnel
|
|
436
427
|
const isRespondingViaTunnel = yield this.isWDARunning(session.wdaPort);
|
|
437
428
|
if (!isRespondingViaTunnel) {
|
|
438
|
-
logger_1.default.warn(`🛡️ [${udid}] [Watchdog] WDA
|
|
439
|
-
const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
|
|
440
|
-
if (device && device.ip) {
|
|
441
|
-
const isWdaAccessibleViaNetwork = yield this.isWDARunningOnHost(device.ip, 8100);
|
|
442
|
-
if (isWdaAccessibleViaNetwork) {
|
|
443
|
-
logger_1.default.info(`🛡️ [${udid}] [Watchdog] WDA is alive on network but tunnel is hung. Restarting tunnels...`);
|
|
444
|
-
yield this.restartTunnelsOnly(session);
|
|
445
|
-
return true;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
429
|
+
logger_1.default.warn(`🛡️ [${udid}] [Watchdog] WDA on port ${session.wdaPort} is unresponsive.`);
|
|
448
430
|
return false;
|
|
449
431
|
}
|
|
450
432
|
return true;
|
|
451
433
|
});
|
|
452
434
|
}
|
|
453
|
-
/**
|
|
454
|
-
* Check if WDA is running on a specific host/port
|
|
455
|
-
*/
|
|
456
|
-
isWDARunningOnHost(host, port) {
|
|
457
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
458
|
-
var _a, _b;
|
|
459
|
-
const axios = (yield Promise.resolve().then(() => __importStar(require('axios')))).default;
|
|
460
|
-
try {
|
|
461
|
-
const response = yield axios.get(`http://${host}:${port}/status`, {
|
|
462
|
-
timeout: 3000,
|
|
463
|
-
validateStatus: (status) => status === 200,
|
|
464
|
-
});
|
|
465
|
-
return ((_b = (_a = response.data) === null || _a === void 0 ? void 0 : _a.value) === null || _b === void 0 ? void 0 : _b.ready) === true;
|
|
466
|
-
}
|
|
467
|
-
catch (e) {
|
|
468
|
-
return false;
|
|
469
|
-
}
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
/**
|
|
473
|
-
* Restarts only the iproxy tunnels without stopping WDA
|
|
474
|
-
*/
|
|
475
|
-
restartTunnelsOnly(session) {
|
|
476
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
477
|
-
const udid = session.udid;
|
|
478
|
-
const isolationService = typedi_2.Container.get(ResourceIsolationService_1.ResourceIsolationService);
|
|
479
|
-
// 1. Kill old tunnels
|
|
480
|
-
if (session.forwardWDAProcess)
|
|
481
|
-
session.forwardWDAProcess.kill('SIGKILL');
|
|
482
|
-
if (session.forwardMJPEGProcess)
|
|
483
|
-
session.forwardMJPEGProcess.kill('SIGKILL');
|
|
484
|
-
// 2. Start new tunnels
|
|
485
|
-
const wdaIproxy = isolationService.wrapSpawn('iproxy', ['-u', udid, `${session.wdaPort}:8100`], 'Performance');
|
|
486
|
-
const mjpegIproxy = isolationService.wrapSpawn('iproxy', ['-u', udid, `${session.mjpegPort}:9100`], 'Performance');
|
|
487
|
-
session.forwardWDAProcess = (0, child_process_1.spawn)(wdaIproxy.command, wdaIproxy.args);
|
|
488
|
-
typedi_2.Container.get(ProcessRegistry_1.ProcessRegistry).track({ kind: 'other', udid, process: session.forwardWDAProcess });
|
|
489
|
-
session.forwardMJPEGProcess = (0, child_process_1.spawn)(mjpegIproxy.command, mjpegIproxy.args);
|
|
490
|
-
typedi_2.Container.get(ProcessRegistry_1.ProcessRegistry).track({ kind: 'ios-mjpeg', udid, process: session.forwardMJPEGProcess });
|
|
491
|
-
logger_1.default.info(`🛡️ [${udid}] [Watchdog] Tunnels restarted successfully.`);
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
435
|
/**
|
|
495
436
|
* Start streaming for a device
|
|
496
437
|
*/
|
|
@@ -92,96 +92,75 @@ let WDAClient = WDAClient_1 = class WDAClient {
|
|
|
92
92
|
}
|
|
93
93
|
const prefixes = (cached === null || cached === void 0 ? void 0 : cached.sessionId) ? [`/session/${cached.sessionId}`] : ['', '/session/any'];
|
|
94
94
|
let lastError;
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
? yield axios_1.default.post(url, data || {}, { timeout })
|
|
130
|
-
: yield axios_1.default.get(url, { timeout });
|
|
131
|
-
// If we reached a host but it returned a logical error (e.g. 404 for session),
|
|
132
|
-
// we should update our cache and potentially stop retrying this host/prefix combo.
|
|
133
|
-
// However, for simplicity, we treat any successful HTTP response as "Host is alive".
|
|
134
|
-
if (isNetworkHost && targetHost !== (cached === null || cached === void 0 ? void 0 : cached.host)) {
|
|
135
|
-
this.log.debug(`[WDA] Successful fallback to network IP ${targetHost} for ${udid}`);
|
|
136
|
-
}
|
|
137
|
-
const sid = ((_a = res.data) === null || _a === void 0 ? void 0 : _a.sessionId) || ((_c = (_b = res.data) === null || _b === void 0 ? void 0 : _b.value) === null || _c === void 0 ? void 0 : _c.sessionId);
|
|
138
|
-
if (sid && sid !== (cached === null || cached === void 0 ? void 0 : cached.sessionId)) {
|
|
139
|
-
this.log.debug(`[WDA] Detected new session ID: ${sid} via ${targetHost}`);
|
|
140
|
-
this.wdaConnectionCache.set(cacheKey, {
|
|
141
|
-
host: targetHost,
|
|
142
|
-
pathPrefix: `/session/${sid}`,
|
|
143
|
-
sessionId: sid,
|
|
144
|
-
});
|
|
145
|
-
typedi_1.Container.get(IOSStreamService_1.default).setWDASessionId(udid, sid);
|
|
146
|
-
}
|
|
147
|
-
return res;
|
|
95
|
+
// Only the phone's own forwarded port. WDA names no phone, so no address
|
|
96
|
+
// on the network is known to be this phone's: a simulator's is this
|
|
97
|
+
// Mac's own, where port 8100 can be an iPhone's WDA forward.
|
|
98
|
+
const targetHost = '127.0.0.1';
|
|
99
|
+
for (const prefix of prefixes) {
|
|
100
|
+
try {
|
|
101
|
+
const isSessionless = !(options === null || options === void 0 ? void 0 : options.useSessionPath) &&
|
|
102
|
+
(['/status', '/health', '/wda/healthcheck'].includes(endpoint));
|
|
103
|
+
const url = `http://${targetHost}:${port}${isSessionless ? '' : prefix}${endpoint}`;
|
|
104
|
+
// Principal Stability: Screenshots, activation, and script execution need longer timeouts.
|
|
105
|
+
// Heartbeats and Status checks should be snappy to fail-over quickly.
|
|
106
|
+
const isHeavyCommand = endpoint.includes('screenshot') ||
|
|
107
|
+
endpoint.includes('source') ||
|
|
108
|
+
endpoint.includes('swipe') ||
|
|
109
|
+
endpoint.includes('touchAndHold') ||
|
|
110
|
+
endpoint.includes('activate') ||
|
|
111
|
+
endpoint.includes('execute') ||
|
|
112
|
+
endpoint.includes('actions');
|
|
113
|
+
const timeout = isHeavyCommand ? 30000 : 5000; // Snappy 5s timeout for standard commands
|
|
114
|
+
const res = method === 'post'
|
|
115
|
+
? yield axios_1.default.post(url, data || {}, { timeout })
|
|
116
|
+
: yield axios_1.default.get(url, { timeout });
|
|
117
|
+
// If we reached a host but it returned a logical error (e.g. 404 for session),
|
|
118
|
+
// we should update our cache and potentially stop retrying this host/prefix combo.
|
|
119
|
+
// However, for simplicity, we treat any successful HTTP response as "Host is alive".
|
|
120
|
+
const sid = ((_a = res.data) === null || _a === void 0 ? void 0 : _a.sessionId) || ((_c = (_b = res.data) === null || _b === void 0 ? void 0 : _b.value) === null || _c === void 0 ? void 0 : _c.sessionId);
|
|
121
|
+
if (sid && sid !== (cached === null || cached === void 0 ? void 0 : cached.sessionId)) {
|
|
122
|
+
this.log.debug(`[WDA] Detected new session ID: ${sid} via ${targetHost}`);
|
|
123
|
+
this.wdaConnectionCache.set(cacheKey, {
|
|
124
|
+
host: targetHost,
|
|
125
|
+
pathPrefix: `/session/${sid}`,
|
|
126
|
+
sessionId: sid,
|
|
127
|
+
});
|
|
128
|
+
typedi_1.Container.get(IOSStreamService_1.default).setWDASessionId(udid, sid);
|
|
148
129
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
this.wdaConnectionCache.delete(cacheKey);
|
|
169
|
-
typedi_1.Container.get(IOSStreamService_1.default).setWDASessionId(udid, '');
|
|
170
|
-
return this.performWDACommand(udid, method, endpoint, data, retryCount + 1, options);
|
|
171
|
-
}
|
|
172
|
-
// Phase 2: Transient State Recovery (500)
|
|
173
|
-
// This handles "Focused" errors, "Internal" errors, or transient locks during app switches.
|
|
174
|
-
if (status === 500) {
|
|
175
|
-
this.log.warn(`[WDA] Transient error (500) for ${udid}, retrying in 1s... (Reason: ${errorMsg})`);
|
|
176
|
-
yield new Promise((r) => setTimeout(r, 1000));
|
|
177
|
-
return this.performWDACommand(udid, method, endpoint, data, retryCount + 1, options);
|
|
130
|
+
return res;
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
lastError = err;
|
|
134
|
+
if (err.response) {
|
|
135
|
+
const status = err.response.status;
|
|
136
|
+
const errorMsg = JSON.stringify(err.response.data);
|
|
137
|
+
this.log.info(`[WDA] Command ${method.toUpperCase()} ${endpoint} failed (${status}): ${errorMsg}`);
|
|
138
|
+
// Principal Resiliency: Multi-Phase Recovery
|
|
139
|
+
if (retryCount < 2) {
|
|
140
|
+
// Phase 1: Dead Session Recovery (404)
|
|
141
|
+
if (status === 404 && prefix.includes('/session/')) {
|
|
142
|
+
// Special Intelligence: Some WDA versions don't support /execute, /wda/homescreen, etc.
|
|
143
|
+
// We should NOT clear the session if these specific commands fail with 404,
|
|
144
|
+
// as they might just be unsupported features.
|
|
145
|
+
const NON_FATAL_ENDPOINTS = ['execute', 'homescreen', 'pressButton'];
|
|
146
|
+
if (NON_FATAL_ENDPOINTS.some((e) => endpoint.includes(e))) {
|
|
147
|
+
this.log.debug(`[WDA] Command ${endpoint} failed with 404 (Unsupported or Logical Error). Skipping session reset.`);
|
|
148
|
+
throw err;
|
|
178
149
|
}
|
|
150
|
+
this.log.warn(`[WDA] Session ${cached === null || cached === void 0 ? void 0 : cached.sessionId} appears dead. Resetting and retrying...`);
|
|
151
|
+
this.wdaConnectionCache.delete(cacheKey);
|
|
152
|
+
typedi_1.Container.get(IOSStreamService_1.default).setWDASessionId(udid, '');
|
|
153
|
+
return this.performWDACommand(udid, method, endpoint, data, retryCount + 1, options);
|
|
154
|
+
}
|
|
155
|
+
// Phase 2: Transient State Recovery (500)
|
|
156
|
+
// This handles "Focused" errors, "Internal" errors, or transient locks during app switches.
|
|
157
|
+
if (status === 500) {
|
|
158
|
+
this.log.warn(`[WDA] Transient error (500) for ${udid}, retrying in 1s... (Reason: ${errorMsg})`);
|
|
159
|
+
yield new Promise((r) => setTimeout(r, 1000));
|
|
160
|
+
return this.performWDACommand(udid, method, endpoint, data, retryCount + 1, options);
|
|
179
161
|
}
|
|
180
|
-
throw err;
|
|
181
162
|
}
|
|
182
|
-
|
|
183
|
-
if (isNetworkHost && ['/status', '/health'].includes(endpoint))
|
|
184
|
-
break;
|
|
163
|
+
throw err;
|
|
185
164
|
}
|
|
186
165
|
}
|
|
187
166
|
}
|
|
@@ -95,7 +95,6 @@ describe("iOS discovery takes only its own host's row for a udid", () => {
|
|
|
95
95
|
});
|
|
96
96
|
svc.trackingInitialized = true; // no usbmux listener
|
|
97
97
|
sinon_1.default.stub(svc, 'getConnectedDevices').resolves(connected);
|
|
98
|
-
sinon_1.default.stub(svc, 'fetchRealDeviceNetworkIp').resolves(''); // no go-ios
|
|
99
98
|
return svc;
|
|
100
99
|
}
|
|
101
100
|
beforeEach(() => {
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
38
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
39
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
40
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
41
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
45
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
|
+
};
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
require("reflect-metadata");
|
|
49
|
+
const chai_1 = require("chai");
|
|
50
|
+
const axios_1 = __importDefault(require("axios"));
|
|
51
|
+
const child_process_1 = __importDefault(require("child_process"));
|
|
52
|
+
const events_1 = require("events");
|
|
53
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
54
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
55
|
+
const typedi_1 = require("typedi");
|
|
56
|
+
const appium_ios_device_1 = require("appium-ios-device");
|
|
57
|
+
const IOSDiscoveryService_1 = require("../../src/device-managers/ios/IOSDiscoveryService");
|
|
58
|
+
const IOSStreamService_1 = __importDefault(require("../../src/device-managers/ios/IOSStreamService"));
|
|
59
|
+
const WDAClient_1 = require("../../src/device-managers/ios/WDAClient");
|
|
60
|
+
const ownDeviceRow = __importStar(require("../../src/device-managers/ownDeviceRow"));
|
|
61
|
+
const device_store_1 = require("../../src/data-service/device-store");
|
|
62
|
+
const deviceUtils = __importStar(require("../../src/device-utils"));
|
|
63
|
+
const PluginContext_1 = require("../../src/PluginContext");
|
|
64
|
+
const PortAllocator_1 = require("../../src/services/PortAllocator");
|
|
65
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
66
|
+
/**
|
|
67
|
+
* Xenon never reaches WebDriverAgent over the network, only through the
|
|
68
|
+
* phone's own forwarded port on 127.0.0.1.
|
|
69
|
+
*
|
|
70
|
+
* An iPhone's network address was meant to come from go-ios `ios info`, which
|
|
71
|
+
* has no such value (its lockdown values carry no IPAddress), so detection
|
|
72
|
+
* asked for it on every pass for nothing. The fallbacks that used it never
|
|
73
|
+
* ran for an iPhone. A simulator's address is the Mac's own, so they sent a
|
|
74
|
+
* simulator's commands to this Mac's port 8100, which can be an iPhone's
|
|
75
|
+
* WebDriverAgent: WDA names no phone.
|
|
76
|
+
*/
|
|
77
|
+
const IP = '192.168.0.104';
|
|
78
|
+
const PHONE = 'test-iphone-00008110-net';
|
|
79
|
+
const ADDRESS = '10.9.8.7';
|
|
80
|
+
describe('iOS: WebDriverAgent only through the phone’s own forwarded port', () => {
|
|
81
|
+
beforeEach(() => {
|
|
82
|
+
sinon_1.default.stub(process, 'kill');
|
|
83
|
+
});
|
|
84
|
+
afterEach(() => sinon_1.default.restore());
|
|
85
|
+
describe('detection', () => {
|
|
86
|
+
let commands;
|
|
87
|
+
function discovery() {
|
|
88
|
+
var _a;
|
|
89
|
+
const ctx = new PluginContext_1.PluginContext();
|
|
90
|
+
ctx.setContext(Object.assign({}, IPluginArgs_1.DefaultPluginArgs, { bindHostOrIp: IP }), 4724, 'test', '');
|
|
91
|
+
const svc = new IOSDiscoveryService_1.IOSDiscoveryService(ctx);
|
|
92
|
+
// resetTestContainer() stubs this on the prototype in a full run.
|
|
93
|
+
const proto = IOSDiscoveryService_1.IOSDiscoveryService.prototype;
|
|
94
|
+
svc.fetchLocalIOSDevices =
|
|
95
|
+
(_a = proto.fetchLocalIOSDevices.wrappedMethod) !== null && _a !== void 0 ? _a : proto.fetchLocalIOSDevices;
|
|
96
|
+
svc.trackingInitialized = true; // no usbmux listener
|
|
97
|
+
sinon_1.default.stub(svc, 'getConnectedDevices').resolves([PHONE]);
|
|
98
|
+
return svc;
|
|
99
|
+
}
|
|
100
|
+
beforeEach(() => {
|
|
101
|
+
commands = [];
|
|
102
|
+
// The go-ios binary is present, and every command it would run is recorded.
|
|
103
|
+
const exists = fs_extra_1.default.existsSync;
|
|
104
|
+
sinon_1.default
|
|
105
|
+
.stub(fs_extra_1.default, 'existsSync')
|
|
106
|
+
.callsFake((p) => String(p).endsWith('goIOS/ios') || exists(p));
|
|
107
|
+
sinon_1.default.stub(child_process_1.default, 'exec').callsFake(((cmd, ...rest) => {
|
|
108
|
+
commands.push(cmd);
|
|
109
|
+
const done = rest.find((a) => typeof a === 'function');
|
|
110
|
+
done === null || done === void 0 ? void 0 : done(null, '{}', '');
|
|
111
|
+
return new events_1.EventEmitter();
|
|
112
|
+
}));
|
|
113
|
+
sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({ findDevice: () => __awaiter(void 0, void 0, void 0, function* () { return null; }) });
|
|
114
|
+
const real = typedi_1.Container.get.bind(typedi_1.Container);
|
|
115
|
+
sinon_1.default.stub(typedi_1.Container, 'get').callsFake((token) => {
|
|
116
|
+
if (token === PortAllocator_1.PortAllocator)
|
|
117
|
+
return { tryAcquire: () => __awaiter(void 0, void 0, void 0, function* () { return 28400; }) };
|
|
118
|
+
if (token === IOSStreamService_1.default)
|
|
119
|
+
return { getStreamStatus: () => undefined };
|
|
120
|
+
return real(token);
|
|
121
|
+
});
|
|
122
|
+
sinon_1.default.stub(deviceUtils, 'getUtilizationTime').resolves(0);
|
|
123
|
+
sinon_1.default.stub(appium_ios_device_1.utilities, 'getOSVersion').resolves('26.5');
|
|
124
|
+
sinon_1.default.stub(appium_ios_device_1.utilities, 'getDeviceName').resolves('iPhone 14 Plus');
|
|
125
|
+
sinon_1.default.stub(appium_ios_device_1.utilities, 'getDeviceInfo').rejects(new Error('no lockdown in tests'));
|
|
126
|
+
});
|
|
127
|
+
const asksForAnAddress = () => commands.filter((c) => /\binfo --udid\b/.test(c));
|
|
128
|
+
it('asks go-ios nothing about a known iPhone on each pass', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
129
|
+
const own = { udid: PHONE, host: `http://${IP}:4724`, platform: 'ios', ip: '' };
|
|
130
|
+
const [phone] = yield discovery().fetchLocalIOSDevices([own]);
|
|
131
|
+
(0, chai_1.expect)(phone.udid).to.equal(PHONE);
|
|
132
|
+
(0, chai_1.expect)(asksForAnAddress()).to.deep.equal([]);
|
|
133
|
+
}));
|
|
134
|
+
it('asks go-ios nothing about the address of an iPhone it adds', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
135
|
+
const phone = yield discovery().getDeviceInfo(PHONE);
|
|
136
|
+
(0, chai_1.expect)(phone.ip).to.equal('');
|
|
137
|
+
(0, chai_1.expect)(asksForAnAddress()).to.deep.equal([]);
|
|
138
|
+
}));
|
|
139
|
+
});
|
|
140
|
+
describe('WDAClient', () => {
|
|
141
|
+
it("retries a failed command on no host but 127.0.0.1, even with the phone's address", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
142
|
+
const urls = [];
|
|
143
|
+
const refuse = (url) => __awaiter(void 0, void 0, void 0, function* () {
|
|
144
|
+
urls.push(url);
|
|
145
|
+
throw Object.assign(new Error(`connect ECONNREFUSED ${url}`), { code: 'ECONNREFUSED' });
|
|
146
|
+
});
|
|
147
|
+
sinon_1.default.stub(axios_1.default, 'post').callsFake(refuse);
|
|
148
|
+
sinon_1.default.stub(axios_1.default, 'get').callsFake(refuse);
|
|
149
|
+
sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({
|
|
150
|
+
findDevice: () => __awaiter(void 0, void 0, void 0, function* () { return ({ udid: PHONE, wdaLocalPort: 28100, ip: ADDRESS }); }),
|
|
151
|
+
});
|
|
152
|
+
const real = typedi_1.Container.get.bind(typedi_1.Container);
|
|
153
|
+
sinon_1.default.stub(typedi_1.Container, 'get').callsFake((token) => {
|
|
154
|
+
if (token === IOSStreamService_1.default) {
|
|
155
|
+
return {
|
|
156
|
+
getStreamStatus: () => undefined,
|
|
157
|
+
getWDASessionId: () => undefined,
|
|
158
|
+
setWDASessionId: () => undefined,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return real(token);
|
|
162
|
+
});
|
|
163
|
+
const err = yield new WDAClient_1.WDAClient()
|
|
164
|
+
.sendWDACommand(PHONE, 'post', '/wda/tap', { x: 10, y: 10 })
|
|
165
|
+
.then(() => null, (e) => e);
|
|
166
|
+
(0, chai_1.expect)(err === null || err === void 0 ? void 0 : err.message).to.match(/ECONNREFUSED/);
|
|
167
|
+
(0, chai_1.expect)(urls).to.not.deep.equal([]);
|
|
168
|
+
(0, chai_1.expect)(urls.filter((u) => !u.startsWith('http://127.0.0.1:28100/'))).to.deep.equal([]);
|
|
169
|
+
}));
|
|
170
|
+
});
|
|
171
|
+
describe('the stream watchdog', () => {
|
|
172
|
+
let spawned;
|
|
173
|
+
function fakeProcess(exitCode) {
|
|
174
|
+
return Object.assign(new events_1.EventEmitter(), { exitCode, kill: () => true });
|
|
175
|
+
}
|
|
176
|
+
function streamWith(forwardExitCode) {
|
|
177
|
+
// Bypass the constructor: it starts watchdog intervals.
|
|
178
|
+
const svc = Object.create(IOSStreamService_1.default.prototype);
|
|
179
|
+
svc.sessions = new Map([
|
|
180
|
+
[
|
|
181
|
+
PHONE,
|
|
182
|
+
{
|
|
183
|
+
udid: PHONE,
|
|
184
|
+
status: 'running',
|
|
185
|
+
wdaPort: 28100,
|
|
186
|
+
mjpegPort: 29100,
|
|
187
|
+
wdaProcess: fakeProcess(null),
|
|
188
|
+
forwardWDAProcess: fakeProcess(forwardExitCode),
|
|
189
|
+
forwardMJPEGProcess: fakeProcess(null),
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
]);
|
|
193
|
+
return svc;
|
|
194
|
+
}
|
|
195
|
+
beforeEach(() => {
|
|
196
|
+
spawned = [];
|
|
197
|
+
sinon_1.default.stub(child_process_1.default, 'spawn').callsFake(((cmd, args) => {
|
|
198
|
+
spawned.push([cmd, ...(args !== null && args !== void 0 ? args : [])].join(' '));
|
|
199
|
+
return fakeProcess(null);
|
|
200
|
+
}));
|
|
201
|
+
// Something answers ready on the phone's address, port 8100; the
|
|
202
|
+
// phone's own forwarded port refuses.
|
|
203
|
+
sinon_1.default.stub(axios_1.default, 'get').callsFake(((url) => __awaiter(void 0, void 0, void 0, function* () {
|
|
204
|
+
if (url.startsWith(`http://${ADDRESS}:`))
|
|
205
|
+
return { status: 200, data: { value: { ready: true } } };
|
|
206
|
+
throw Object.assign(new Error('connect ECONNREFUSED'), { code: 'ECONNREFUSED' });
|
|
207
|
+
})));
|
|
208
|
+
sinon_1.default.stub(ownDeviceRow, 'findOwnDevice').resolves({ udid: PHONE, ip: ADDRESS });
|
|
209
|
+
});
|
|
210
|
+
it('calls a stream whose forward died unresponsive, whatever answers on the network', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
211
|
+
(0, chai_1.expect)(yield streamWith(1).isStreamResponsive(PHONE)).to.equal(false);
|
|
212
|
+
(0, chai_1.expect)(spawned, 'no forward-only restart').to.deep.equal([]);
|
|
213
|
+
}));
|
|
214
|
+
it('calls a stream whose forward is hung unresponsive, whatever answers on the network', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
215
|
+
(0, chai_1.expect)(yield streamWith(null).isStreamResponsive(PHONE)).to.equal(false);
|
|
216
|
+
(0, chai_1.expect)(spawned, 'no forward-only restart').to.deep.equal([]);
|
|
217
|
+
}));
|
|
218
|
+
});
|
|
219
|
+
});
|