@xenon-device-management/xenon 2.8.2 → 2.9.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/lib/package.json +1 -1
- package/lib/public/assets/{AnnotationOverlay-BqBip3BK.js → AnnotationOverlay-DhRFPZ9-.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-DUvekoqc.js → ApiKeyGate-BmZy1Srn.js} +1 -1
- package/lib/public/assets/{BugReportButton-BG0CrInp.js → BugReportButton-ByLSqwSk.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-B0SnOzKM.js → DeviceMosaicView-DVfXSoXH.js} +2 -2
- package/lib/public/assets/{EmptyState-Dq-Bq8TT.js → EmptyState-Bib6wal8.js} +1 -1
- package/lib/public/assets/{FieldGroup-BIgXTGgC.js → FieldGroup-D5sTaKGR.js} +1 -1
- package/lib/public/assets/{FilterMenu-CeJOFd6_.js → FilterMenu-D3XXKTwF.js} +1 -1
- package/lib/public/assets/{Menu-B4H6_Xk9.js → Menu-BiZhr0F7.js} +1 -1
- package/lib/public/assets/{Modal-DJG1w3KG.js → Modal-BxGNa3Zp.js} +1 -1
- package/lib/public/assets/{RecordingPage-CuzbuRlh.js → RecordingPage-Czml77Wb.js} +1 -1
- package/lib/public/assets/{RecordingsPage-CW2IMjO4.js → RecordingsPage-Di7nyHqE.js} +1 -1
- package/lib/public/assets/{SegmentedControl-DhQ3uYdc.js → SegmentedControl-XEKvCRwO.js} +1 -1
- package/lib/public/assets/{SettingCard-C5Gmj98i.js → SettingCard-YIpOQAjO.js} +1 -1
- package/lib/public/assets/{Table-D-WtrnfJ.js → Table-CqwX6v-1.js} +1 -1
- package/lib/public/assets/{activity-CvT65tfF.js → activity-Cur7Syd6.js} +1 -1
- package/lib/public/assets/{ai-settings-DuCpE_dA.js → ai-settings-B1OGdSKu.js} +1 -1
- package/lib/public/assets/{api-keys-DEIUB9Bz.js → api-keys-abTul2Oa.js} +1 -1
- package/lib/public/assets/{apps-B0z6NLY9.js → apps-yQL1MLfG.js} +1 -1
- package/lib/public/assets/{arrow-left-CCUPd12W.js → arrow-left-D0ipNN6o.js} +1 -1
- package/lib/public/assets/{arrow-right-CrLsc2tN.js → arrow-right-DwVz_Fly.js} +1 -1
- package/lib/public/assets/{arrow-up-right-CiQmHVQo.js → arrow-up-right-xKuKJfZf.js} +1 -1
- package/lib/public/assets/{auth-shell-CHgiSQ9B.js → auth-shell-00zHLFCk.js} +2 -2
- package/lib/public/assets/{builds-page-BvxRnAet.js → builds-page-D4RJAbOZ.js} +1 -1
- package/lib/public/assets/{button-pgE9Zb7y.js → button-Cic7S8s2.js} +1 -1
- package/lib/public/assets/{calendar-CNJBqo5s.js → calendar-DyNZ2bq9.js} +1 -1
- package/lib/public/assets/{check-BOAfxviA.js → check-CXGZdojb.js} +1 -1
- package/lib/public/assets/{chevron-left-DAYay1pY.js → chevron-left-BcNiIt1C.js} +1 -1
- package/lib/public/assets/{chevron-right-B0FDOdzU.js → chevron-right-BxDjkb1I.js} +1 -1
- package/lib/public/assets/{circle-check-DOIAC8XC.js → circle-check-9isFAWQ6.js} +1 -1
- package/lib/public/assets/{circle-x-z8FaSlcA.js → circle-x-CfnPCFLK.js} +1 -1
- package/lib/public/assets/{clock-BtaZSeEx.js → clock-Dc4TvnFF.js} +1 -1
- package/lib/public/assets/{copy-D60KmcdV.js → copy-TFQpalJ2.js} +1 -1
- package/lib/public/assets/{device-explorer-Cph43t8H.js → device-explorer-CB4bSJtl.js} +1 -1
- package/lib/public/assets/{download-s-xM4AGY.js → download-1_bq_Ikd.js} +1 -1
- package/lib/public/assets/{forgot-password-CKw6RVHP.js → forgot-password-B92TZ1GF.js} +1 -1
- package/lib/public/assets/{funnel-BvgEGRdQ.js → funnel-DC7DsjHe.js} +1 -1
- package/lib/public/assets/{index-BB9sO7Nt.js → index-BSsnx09Q.js} +1 -1
- package/lib/public/assets/{index-BpIf5nE2.js → index-CC2N_uMU.js} +2 -2
- package/lib/public/assets/{input-CPS4hChq.js → input-D2DBpxK2.js} +1 -1
- package/lib/public/assets/{keycodes-D5ZobFRW.js → keycodes-BQuIOMw0.js} +1 -1
- package/lib/public/assets/{layers-CrAsrOG4.js → layers-DxtoRlvi.js} +1 -1
- package/lib/public/assets/{list-checks-BnEjyKfv.js → list-checks-jQ7YUgdE.js} +1 -1
- package/lib/public/assets/{lock-Dm0hfbuM.js → lock-WHSsj2dj.js} +1 -1
- package/lib/public/assets/{login-DdlbwQwy.js → login-BB_xyUWw.js} +1 -1
- package/lib/public/assets/{maintenance-settings-kocnslUP.js → maintenance-settings-Dwadp8IQ.js} +1 -1
- package/lib/public/assets/{monitor-CGU-u9zq.js → monitor-DIWQjUlh.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CWu4a7Ez.js → mouse-pointer-2-D11Re9Gj.js} +1 -1
- package/lib/public/assets/{network-TV7kV1jl.js → network-BYBgSBjv.js} +1 -1
- package/lib/public/assets/{overview-C3pWhCtz.js → overview-BISAwoyi.js} +1 -1
- package/lib/public/assets/{page-header-DCNc9cYT.js → page-header-ChX4YiMP.js} +1 -1
- package/lib/public/assets/{plus-C6Uuyu5Z.js → plus-BWNj1XSQ.js} +1 -1
- package/lib/public/assets/{profile-page-CjUENuCw.js → profile-page-VZD3gRjV.js} +1 -1
- package/lib/public/assets/{reset-password-rpbIm1Go.js → reset-password-Dz9LzFeX.js} +1 -1
- package/lib/public/assets/{runbook-page-BGkOmIkY.js → runbook-page-Bl2FodZf.js} +1 -1
- package/lib/public/assets/{select-6Q8XgEfq.js → select-Bs9CQu5Z.js} +1 -1
- package/lib/public/assets/{selector-detail-page-BPIr14Yp.js → selector-detail-page-By7ihhbY.js} +1 -1
- package/lib/public/assets/{selector-health-page-Vycvp0ED.js → selector-health-page-Y4CTbOLH.js} +1 -1
- package/lib/public/assets/{selector-health-DhZ490H5.js → selector-health-xiBb8vNz.js} +1 -1
- package/lib/public/assets/{session-detail-page-D2rvJIgL.js → session-detail-page-DqeKRP6c.js} +1 -1
- package/lib/public/assets/{settings-bF2S7k19.js → settings-DDhV_Dnz.js} +1 -1
- package/lib/public/assets/stat-tile-DMBTDbwt.js +8 -0
- package/lib/public/assets/{tablet-1Gmx6FV4.js → tablet-Cr5bOgng.js} +1 -1
- package/lib/public/assets/{teams-DHBNN_a7.js → teams-Bm0UQkR6.js} +1 -1
- package/lib/public/assets/{trash-2-D3idPlqv.js → trash-2-Di_lMeiP.js} +1 -1
- package/lib/public/assets/{upload-HOW_42FA.js → upload-RvXDBmCX.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-CVPBPGSb.js → use-password-reset-mode-C7pUQ7xe.js} +1 -1
- package/lib/public/assets/{users-DjGgZFe_.js → users-CIILdbvy.js} +1 -1
- package/lib/public/assets/{users-BnKiArxK.js → users-DZ7B5ePh.js} +1 -1
- package/lib/public/assets/{video-off-CuipXFJK.js → video-off-aYil8B-X.js} +1 -1
- package/lib/public/assets/{webhook-settings-7_s2Zky-.js → webhook-settings-17JYW1ZR.js} +1 -1
- package/lib/public/assets/{zap-Gl0D1sD4.js → zap-D9hchm-a.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/app/routers/dashboard.js +6 -0
- package/lib/src/app/routers/sessionInsights.js +23 -1
- package/lib/src/device-managers/ios/IOSStreamService.js +12 -0
- package/lib/src/device-managers/ios/IOSTunnels.js +147 -23
- package/lib/src/device-managers/ios/WDAClient.js +18 -4
- package/lib/test/unit/ios-go-ios-tunnel-env.spec.js +38 -1
- package/lib/test/unit/ios-stream-appium-tunnel.spec.js +10 -0
- package/lib/test/unit/ios-stream-port-leases.spec.js +26 -0
- package/lib/test/unit/ios-tunnels.spec.js +90 -0
- package/lib/test/unit/session-list-insights.spec.js +57 -0
- package/package.json +1 -1
- package/lib/public/assets/stat-tile-DOrnbGTr.js +0 -8
|
@@ -129,7 +129,13 @@ function getSessions(request, response) {
|
|
|
129
129
|
const cursor = readQuery(response, () => (0, sessionInsights_1.cursorWhere)(request.query.before, request.query.beforeId));
|
|
130
130
|
if (cursor === undefined)
|
|
131
131
|
return;
|
|
132
|
+
const ids = readQuery(response, () => (0, sessionInsights_1.parseIds)(request.query.ids));
|
|
133
|
+
if (ids === undefined)
|
|
134
|
+
return;
|
|
132
135
|
const where = {};
|
|
136
|
+
if (ids) {
|
|
137
|
+
where.id = { in: ids };
|
|
138
|
+
}
|
|
133
139
|
if (since) {
|
|
134
140
|
where.createdAt = { gte: since };
|
|
135
141
|
}
|
|
@@ -9,10 +9,11 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.SESSION_PAGE_MAX = exports.SESSION_PAGE_DEFAULT = exports.InvalidQueryError = void 0;
|
|
12
|
+
exports.SESSION_IDS_MAX = exports.SESSION_PAGE_MAX = exports.SESSION_PAGE_DEFAULT = exports.InvalidQueryError = void 0;
|
|
13
13
|
exports.sessionOutcome = sessionOutcome;
|
|
14
14
|
exports.parseSince = parseSince;
|
|
15
15
|
exports.parseLimit = parseLimit;
|
|
16
|
+
exports.parseIds = parseIds;
|
|
16
17
|
exports.cursorWhere = cursorWhere;
|
|
17
18
|
exports.describeSessions = describeSessions;
|
|
18
19
|
exports.percentile = percentile;
|
|
@@ -77,6 +78,27 @@ function parseLimit(raw) {
|
|
|
77
78
|
}
|
|
78
79
|
return Math.min(n, exports.SESSION_PAGE_MAX);
|
|
79
80
|
}
|
|
81
|
+
/** The most ids one GET /session takes. */
|
|
82
|
+
exports.SESSION_IDS_MAX = 200;
|
|
83
|
+
/**
|
|
84
|
+
* The `ids` query parameter: null when absent; else the comma-separated
|
|
85
|
+
* session ids, 1 to SESSION_IDS_MAX of them, or InvalidQueryError. The
|
|
86
|
+
* Sessions page re-reads older rows still running by id.
|
|
87
|
+
*/
|
|
88
|
+
function parseIds(raw) {
|
|
89
|
+
if (raw === undefined)
|
|
90
|
+
return null;
|
|
91
|
+
const ids = typeof raw === 'string'
|
|
92
|
+
? raw
|
|
93
|
+
.split(',')
|
|
94
|
+
.map((id) => id.trim())
|
|
95
|
+
.filter(Boolean)
|
|
96
|
+
: [];
|
|
97
|
+
if (ids.length === 0 || ids.length > exports.SESSION_IDS_MAX) {
|
|
98
|
+
throw new InvalidQueryError('invalid_ids', `ids must list 1 to ${exports.SESSION_IDS_MAX} session ids, separated by commas`);
|
|
99
|
+
}
|
|
100
|
+
return ids;
|
|
101
|
+
}
|
|
80
102
|
/**
|
|
81
103
|
* Where the next page starts: after the last row of the page before, given
|
|
82
104
|
* as its `createdAt` (`before`) and `id` (`beforeId`). Pages go newest first
|
|
@@ -283,6 +283,10 @@ let IOSStreamService = class IOSStreamService {
|
|
|
283
283
|
return __awaiter(this, void 0, void 0, function* () {
|
|
284
284
|
if (yield this.appiumSessionMayUse(udid, 'go-ios tunnels'))
|
|
285
285
|
return;
|
|
286
|
+
// A tunnel opened for screenshots is live and tracked, not a leftover:
|
|
287
|
+
// the start's ensure takes it over (IOSTunnels.borrow).
|
|
288
|
+
if (this.tunnels().isOnDemand(udid))
|
|
289
|
+
return;
|
|
286
290
|
logger_1.default.debug(`Cleaning up orphan tunnels for ${udid}...`);
|
|
287
291
|
yield this.tunnels().stop(udid);
|
|
288
292
|
// Reap the tunnel process *group* for this udid so the self-forking go-ios
|
|
@@ -853,6 +857,14 @@ let IOSStreamService = class IOSStreamService {
|
|
|
853
857
|
session.status = 'error';
|
|
854
858
|
session.lastError = error.message;
|
|
855
859
|
}
|
|
860
|
+
// A start that got its tunnel up and then failed (WDA didn't launch)
|
|
861
|
+
// stops that tunnel, so it doesn't run on, holding its ports, until
|
|
862
|
+
// the phone's next start. Not while an Appium session may use it.
|
|
863
|
+
if ((session === null || session === void 0 ? void 0 : session.tunnelPort) != null &&
|
|
864
|
+
!(yield this.appiumSessionMayUse(udid, "the failed start's go-ios tunnel"))) {
|
|
865
|
+
yield this.tunnels().stop(udid);
|
|
866
|
+
session.tunnelPort = null;
|
|
867
|
+
}
|
|
856
868
|
// Back off before the next attempt so a polling client can't drive a
|
|
857
869
|
// restart loop against a device that keeps failing to start.
|
|
858
870
|
this.markRecoveryAttempt(udid);
|
|
@@ -18,8 +18,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
18
18
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
19
|
};
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
exports.IOSTunnels = exports.TUNNEL_CHECK_MS = exports.TUNNEL_LEASE_TTL_MS = exports.TUNNEL_READY_POLL_MS = exports.TUNNEL_READY_TIMEOUT_MS = void 0;
|
|
21
|
+
exports.IOSTunnels = exports.TUNNEL_BORROW_IDLE_MS = exports.TUNNEL_CHECK_MS = exports.TUNNEL_LEASE_TTL_MS = exports.TUNNEL_READY_POLL_MS = exports.TUNNEL_READY_TIMEOUT_MS = void 0;
|
|
22
22
|
exports.iosVersionOf = iosVersionOf;
|
|
23
|
+
exports.goIosReason = goIosReason;
|
|
23
24
|
/**
|
|
24
25
|
* One go-ios tunnel per iOS 17+ phone, each on its own leased pair of ports.
|
|
25
26
|
*
|
|
@@ -68,12 +69,53 @@ exports.TUNNEL_READY_POLL_MS = 500;
|
|
|
68
69
|
exports.TUNNEL_LEASE_TTL_MS = 90 * 60 * 1000;
|
|
69
70
|
/** How often running tunnels are checked for a phone that went away. */
|
|
70
71
|
exports.TUNNEL_CHECK_MS = 5000;
|
|
72
|
+
/**
|
|
73
|
+
* How long a tunnel opened for screenshots stays after the last one: long
|
|
74
|
+
* enough that a client taking screenshots in a loop pays the start once.
|
|
75
|
+
*/
|
|
76
|
+
exports.TUNNEL_BORROW_IDLE_MS = 2 * 60 * 1000;
|
|
77
|
+
/** How long a phone's iOS version is trusted before `ios info` is asked again. */
|
|
78
|
+
const VERSION_CACHE_MS = 10 * 60 * 1000;
|
|
71
79
|
/** The iOS version `ios info` reports, as a number: 17.2 for "17.2.1" or "iOS 17.2", 0 for none. */
|
|
72
80
|
function iosVersionOf(info) {
|
|
73
81
|
const versionStr = String(info.ProductVersion || info.HumanReadableProductVersionString || '0');
|
|
74
82
|
const match = versionStr.match(/(\d+\.?\d*)/);
|
|
75
83
|
return match ? parseFloat(match[0]) : 0;
|
|
76
84
|
}
|
|
85
|
+
/** The longest go-ios reason kept for an error message. */
|
|
86
|
+
const REASON_MAX = 200;
|
|
87
|
+
/**
|
|
88
|
+
* The last error or warning in a chunk of go-ios's stderr, for an error
|
|
89
|
+
* message. go-ios logs JSON lines: an ERROR or WARN line gives its `msg` and
|
|
90
|
+
* `error`, an INFO line gives nothing, and a line that isn't JSON (a panic)
|
|
91
|
+
* is kept as it is.
|
|
92
|
+
*/
|
|
93
|
+
function goIosReason(text) {
|
|
94
|
+
var _a;
|
|
95
|
+
let reason;
|
|
96
|
+
for (const raw of text.split('\n')) {
|
|
97
|
+
const line = raw.trim();
|
|
98
|
+
if (!line)
|
|
99
|
+
continue;
|
|
100
|
+
let entry;
|
|
101
|
+
try {
|
|
102
|
+
entry = JSON.parse(line);
|
|
103
|
+
}
|
|
104
|
+
catch (_b) {
|
|
105
|
+
entry = undefined;
|
|
106
|
+
}
|
|
107
|
+
if (entry && typeof entry === 'object') {
|
|
108
|
+
const level = String((_a = entry.level) !== null && _a !== void 0 ? _a : '').toUpperCase();
|
|
109
|
+
if ((level === 'ERROR' || level === 'WARN') && entry.msg) {
|
|
110
|
+
reason = entry.error ? `${entry.msg}: ${entry.error}` : String(entry.msg);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
reason = line;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return reason === null || reason === void 0 ? void 0 : reason.slice(0, REASON_MAX);
|
|
118
|
+
}
|
|
77
119
|
/** An answer that puts the phone's traffic anywhere but its leased P + 1. */
|
|
78
120
|
function moved(info, port) {
|
|
79
121
|
return info.state === 'up' && info.userspacePort !== undefined && info.userspacePort !== port + 1;
|
|
@@ -83,29 +125,48 @@ let IOSTunnels = class IOSTunnels {
|
|
|
83
125
|
this.log = logger_1.default.scope('IOSTunnels');
|
|
84
126
|
this.tunnels = new Map();
|
|
85
127
|
this.checking = false;
|
|
128
|
+
this.versionCache = new Map();
|
|
86
129
|
this.goIOSPath = path_1.default.join((0, helpers_1.cachePath)('goIOS'), 'ios');
|
|
87
130
|
}
|
|
88
131
|
/**
|
|
89
|
-
* The phone's tunnel-info port, starting its tunnel if it
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
132
|
+
* The phone's tunnel-info port for its stream, starting its tunnel if it
|
|
133
|
+
* has none. A tunnel opened for screenshots is taken over: from then on
|
|
134
|
+
* only the stream's stop ends it. Null for a phone below iOS 17, or whose
|
|
135
|
+
* version can't be read: it needs no tunnel. Throws when no pair of ports
|
|
136
|
+
* is left, or when the tunnel exits before it is ready.
|
|
93
137
|
*/
|
|
94
138
|
ensure(udid) {
|
|
95
139
|
return __awaiter(this, void 0, void 0, function* () {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
140
|
+
return this.open(udid, 'stream');
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The phone's tunnel-info port for a screenshot, which on iOS 17+ is
|
|
145
|
+
* reached only through the tunnel. A phone that isn't streaming has none,
|
|
146
|
+
* so one is started; it stays while screenshots keep coming and stops
|
|
147
|
+
* {@link TUNNEL_BORROW_IDLE_MS} after the last one (checkTunnels). Null and
|
|
148
|
+
* throws as ensure.
|
|
149
|
+
*/
|
|
150
|
+
borrow(udid) {
|
|
151
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
152
|
+
return this.open(udid, 'screenshot');
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/** Whether the phone's tunnel was opened for screenshots and no stream has taken it. */
|
|
156
|
+
isOnDemand(udid) {
|
|
157
|
+
const tunnel = this.tunnels.get(udid);
|
|
158
|
+
return !!tunnel && tunnel.process.exitCode === null && !tunnel.streamOwned;
|
|
159
|
+
}
|
|
160
|
+
open(udid, use) {
|
|
161
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
162
|
+
const running = this.tunnels.get(udid);
|
|
163
|
+
if (running && running.process.exitCode === null) {
|
|
164
|
+
this.claim(running, use);
|
|
165
|
+
return running.whenReady;
|
|
107
166
|
}
|
|
108
|
-
|
|
167
|
+
// A screenshot also asks on a simulator, which has no version to read.
|
|
168
|
+
const version = yield this.versionOf(udid, use === 'screenshot');
|
|
169
|
+
if (version === null || version < 17)
|
|
109
170
|
return null;
|
|
110
171
|
const port = yield this.allocator().acquirePair('tunnel', udid, {
|
|
111
172
|
ttlMs: exports.TUNNEL_LEASE_TTL_MS,
|
|
@@ -126,7 +187,15 @@ let IOSTunnels = class IOSTunnels {
|
|
|
126
187
|
yield this.releasePair(port, udid);
|
|
127
188
|
throw e;
|
|
128
189
|
}
|
|
129
|
-
const tunnel = {
|
|
190
|
+
const tunnel = {
|
|
191
|
+
port,
|
|
192
|
+
process: proc,
|
|
193
|
+
ready: false,
|
|
194
|
+
whenReady: Promise.resolve(port),
|
|
195
|
+
streamOwned: false,
|
|
196
|
+
borrowedUntil: 0,
|
|
197
|
+
};
|
|
198
|
+
this.claim(tunnel, use);
|
|
130
199
|
this.tunnels.set(udid, tunnel);
|
|
131
200
|
this.watch();
|
|
132
201
|
// A tunnel whose process ends (go-ios crashed, or something killed it)
|
|
@@ -144,14 +213,30 @@ let IOSTunnels = class IOSTunnels {
|
|
|
144
213
|
this.log.warn(`[${udid}] go-ios tunnel failed: ${err.message}`);
|
|
145
214
|
ended();
|
|
146
215
|
});
|
|
147
|
-
this.logOutput(udid,
|
|
148
|
-
this.log.info(`[${udid}] iOS ${version}: starting a go-ios tunnel on ${port} (traffic on ${port + 1})`
|
|
216
|
+
this.logOutput(udid, tunnel);
|
|
217
|
+
this.log.info(`[${udid}] iOS ${version}: starting a go-ios tunnel on ${port} (traffic on ${port + 1})` +
|
|
218
|
+
(use === 'screenshot' ? ' for screenshots' : ''));
|
|
219
|
+
// Whoever asks while it starts waits for the same start.
|
|
220
|
+
tunnel.whenReady = this.waitUntilReady(udid, tunnel);
|
|
221
|
+
return tunnel.whenReady;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
claim(tunnel, use) {
|
|
225
|
+
if (use === 'stream')
|
|
226
|
+
tunnel.streamOwned = true;
|
|
227
|
+
else
|
|
228
|
+
tunnel.borrowedUntil = this.now() + exports.TUNNEL_BORROW_IDLE_MS;
|
|
229
|
+
}
|
|
230
|
+
waitUntilReady(udid, tunnel) {
|
|
231
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
232
|
+
const { port, process: proc } = tunnel;
|
|
149
233
|
const polls = exports.TUNNEL_READY_TIMEOUT_MS / exports.TUNNEL_READY_POLL_MS;
|
|
150
234
|
for (let poll = 0; poll <= polls; poll++) {
|
|
151
235
|
if (poll > 0)
|
|
152
236
|
yield this.sleep(exports.TUNNEL_READY_POLL_MS);
|
|
153
237
|
if (this.tunnels.get(udid) !== tunnel) {
|
|
154
|
-
throw new Error(`The go-ios tunnel for ${udid} exited before it was ready (exit code ${proc.exitCode})`
|
|
238
|
+
throw new Error(`The go-ios tunnel for ${udid} exited before it was ready (exit code ${proc.exitCode})` +
|
|
239
|
+
(tunnel.reason ? `: ${tunnel.reason}` : ''));
|
|
155
240
|
}
|
|
156
241
|
const info = yield this.tunnelInfo(port, udid);
|
|
157
242
|
if (moved(info, port)) {
|
|
@@ -170,6 +255,33 @@ let IOSTunnels = class IOSTunnels {
|
|
|
170
255
|
return port;
|
|
171
256
|
});
|
|
172
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* The phone's iOS version, from `ios info`, kept for VERSION_CACHE_MS: a
|
|
260
|
+
* screenshot on a phone below iOS 17 would otherwise ask every time. Null
|
|
261
|
+
* when it can't be read (not cached, so a phone still pairing is asked
|
|
262
|
+
* again); logged at debug when `quiet`.
|
|
263
|
+
*/
|
|
264
|
+
versionOf(udid_1) {
|
|
265
|
+
return __awaiter(this, arguments, void 0, function* (udid, quiet = false) {
|
|
266
|
+
var _a;
|
|
267
|
+
const cached = this.versionCache.get(udid);
|
|
268
|
+
if (cached && this.now() - cached.at < VERSION_CACHE_MS)
|
|
269
|
+
return cached.version;
|
|
270
|
+
try {
|
|
271
|
+
const version = yield this.iosVersion(udid);
|
|
272
|
+
this.versionCache.set(udid, { version, at: this.now() });
|
|
273
|
+
return version;
|
|
274
|
+
}
|
|
275
|
+
catch (e) {
|
|
276
|
+
const message = `[${udid}] Could not read the iOS version, so no go-ios tunnel: ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`;
|
|
277
|
+
if (quiet)
|
|
278
|
+
this.log.debug(message);
|
|
279
|
+
else
|
|
280
|
+
this.log.warn(message);
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
173
285
|
/** The phone's tunnel-info port, while its tunnel runs. */
|
|
174
286
|
portFor(udid) {
|
|
175
287
|
const tunnel = this.tunnels.get(udid);
|
|
@@ -217,6 +329,11 @@ let IOSTunnels = class IOSTunnels {
|
|
|
217
329
|
this.checking = true;
|
|
218
330
|
try {
|
|
219
331
|
for (const [udid, tunnel] of [...this.tunnels]) {
|
|
332
|
+
if (!tunnel.streamOwned && tunnel.borrowedUntil <= this.now()) {
|
|
333
|
+
this.log.info(`[${udid}] go-ios tunnel on ${tunnel.port}, opened for screenshots, is idle; stopping it`);
|
|
334
|
+
yield this.stop(udid);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
220
337
|
const info = yield this.tunnelInfo(tunnel.port, udid);
|
|
221
338
|
if (this.tunnels.get(udid) !== tunnel)
|
|
222
339
|
continue;
|
|
@@ -278,13 +395,16 @@ let IOSTunnels = class IOSTunnels {
|
|
|
278
395
|
}
|
|
279
396
|
/**
|
|
280
397
|
* go-ios's output, at debug. Its repeated warning about a connected
|
|
281
|
-
* pre-iOS 17 phone (which needs no tunnel) is logged once per phone.
|
|
398
|
+
* pre-iOS 17 phone (which needs no tunnel) is logged once per phone. Its
|
|
399
|
+
* last error or warning is kept, for the error if the tunnel fails.
|
|
282
400
|
*/
|
|
283
|
-
logOutput(udid,
|
|
401
|
+
logOutput(udid, tunnel) {
|
|
284
402
|
var _a, _b;
|
|
403
|
+
const proc = tunnel.process;
|
|
285
404
|
(_a = proc.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => this.log.debug(`Tunnel [${udid}]: ${data}`));
|
|
286
405
|
const loggedUnsupported = new Set();
|
|
287
406
|
(_b = proc.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => {
|
|
407
|
+
var _a;
|
|
288
408
|
const text = String(data);
|
|
289
409
|
const { unsupported, udid: targetUdid } = (0, iosStreamDiagnostics_1.classifyTunnelStderr)(text);
|
|
290
410
|
if (unsupported) {
|
|
@@ -295,6 +415,7 @@ let IOSTunnels = class IOSTunnels {
|
|
|
295
415
|
}
|
|
296
416
|
return;
|
|
297
417
|
}
|
|
418
|
+
tunnel.reason = (_a = goIosReason(text)) !== null && _a !== void 0 ? _a : tunnel.reason;
|
|
298
419
|
this.log.debug(`Tunnel Err [${udid}]: ${text}`);
|
|
299
420
|
});
|
|
300
421
|
}
|
|
@@ -360,6 +481,9 @@ let IOSTunnels = class IOSTunnels {
|
|
|
360
481
|
req.on('error', () => resolve({ state: 'unknown' }));
|
|
361
482
|
});
|
|
362
483
|
}
|
|
484
|
+
now() {
|
|
485
|
+
return Date.now();
|
|
486
|
+
}
|
|
363
487
|
sleep(ms) {
|
|
364
488
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
365
489
|
}
|
|
@@ -395,12 +395,20 @@ let WDAClient = WDAClient_1 = class WDAClient {
|
|
|
395
395
|
return __awaiter(this, void 0, void 0, function* () {
|
|
396
396
|
var _a, _b, _c;
|
|
397
397
|
const s = typedi_1.Container.get(IOSStreamService_1.default);
|
|
398
|
+
let tunnelError;
|
|
398
399
|
if (yield s.isGoIOSAvailable()) {
|
|
399
400
|
try {
|
|
401
|
+
// On iOS 17+ go-ios reaches the screenshot service only through the
|
|
402
|
+
// phone's own tunnel, and a phone that isn't streaming has none: open
|
|
403
|
+
// one, kept while screenshots keep coming (see IOSTunnels.borrow).
|
|
404
|
+
const tunnels = typedi_1.Container.get(IOSTunnels_1.IOSTunnels);
|
|
405
|
+
yield tunnels.borrow(udid).catch((e) => {
|
|
406
|
+
tunnelError = e;
|
|
407
|
+
throw e;
|
|
408
|
+
});
|
|
400
409
|
const p = path_1.default.join(os_1.default.tmpdir(), `screenshot-${udid}.png`);
|
|
401
410
|
yield execFilePromise(s.goIOSPath, ['screenshot', '--udid', udid, '--output', p], {
|
|
402
|
-
|
|
403
|
-
env: typedi_1.Container.get(IOSTunnels_1.IOSTunnels).envFor(udid),
|
|
411
|
+
env: tunnels.envFor(udid),
|
|
404
412
|
});
|
|
405
413
|
const b = yield fs_extra_1.default.readFile(p);
|
|
406
414
|
yield fs_extra_1.default.remove(p);
|
|
@@ -410,8 +418,14 @@ let WDAClient = WDAClient_1 = class WDAClient {
|
|
|
410
418
|
this.log.debug(`go-ios screenshot failed for ${udid}: ${e.message}\n${e.stack}`);
|
|
411
419
|
}
|
|
412
420
|
}
|
|
413
|
-
|
|
414
|
-
|
|
421
|
+
try {
|
|
422
|
+
const res = yield this.sendWDACommand(udid, 'get', '/screenshot');
|
|
423
|
+
return ((_b = (_a = res.data) === null || _a === void 0 ? void 0 : _a.value) === null || _b === void 0 ? void 0 : _b.screenshot) || ((_c = res.data) === null || _c === void 0 ? void 0 : _c.value) || '';
|
|
424
|
+
}
|
|
425
|
+
catch (e) {
|
|
426
|
+
// WebDriverAgent couldn't answer either; the tunnel's reason says more.
|
|
427
|
+
throw tunnelError !== null && tunnelError !== void 0 ? tunnelError : e;
|
|
428
|
+
}
|
|
415
429
|
});
|
|
416
430
|
}
|
|
417
431
|
getClipboard(udid) {
|
|
@@ -43,13 +43,33 @@ case "$1" in
|
|
|
43
43
|
syslog) printf '{"msg":"port=%s"}\\n' "$GO_IOS_AGENT_PORT"; exec sleep 5 ;;
|
|
44
44
|
esac
|
|
45
45
|
`;
|
|
46
|
-
/**
|
|
46
|
+
/** iOS 17+, not streaming: it has a tunnel only once one is opened for it. */
|
|
47
|
+
const NEEDS_TUNNEL = 'test-iphone-idle-00008120';
|
|
48
|
+
/**
|
|
49
|
+
* IOSTunnels as if WITH_TUNNEL's tunnel ran on 12100, NO_TUNNEL needed none
|
|
50
|
+
* (below iOS 17), and NEEDS_TUNNEL got one on 12102 once borrowed.
|
|
51
|
+
*/
|
|
47
52
|
const tunnels = {
|
|
53
|
+
borrowed: new Set(),
|
|
54
|
+
borrowCalls: [],
|
|
55
|
+
borrowError: undefined,
|
|
56
|
+
borrow(udid) {
|
|
57
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
58
|
+
this.borrowCalls.push(udid);
|
|
59
|
+
if (this.borrowError)
|
|
60
|
+
throw this.borrowError;
|
|
61
|
+
if (udid === NEEDS_TUNNEL)
|
|
62
|
+
this.borrowed.add(udid);
|
|
63
|
+
return udid === WITH_TUNNEL ? 12100 : udid === NEEDS_TUNNEL ? 12102 : null;
|
|
64
|
+
});
|
|
65
|
+
},
|
|
48
66
|
envFor(udid) {
|
|
49
67
|
const env = Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' });
|
|
50
68
|
delete env.GO_IOS_AGENT_PORT;
|
|
51
69
|
if (udid === WITH_TUNNEL)
|
|
52
70
|
env.GO_IOS_AGENT_PORT = '12100';
|
|
71
|
+
if (this.borrowed.has(udid))
|
|
72
|
+
env.GO_IOS_AGENT_PORT = '12102';
|
|
53
73
|
return env;
|
|
54
74
|
},
|
|
55
75
|
};
|
|
@@ -63,6 +83,9 @@ describe("go-ios commands find their own phone's tunnel (GO_IOS_AGENT_PORT)", ()
|
|
|
63
83
|
});
|
|
64
84
|
after(() => fs_1.default.rmSync(dir, { recursive: true, force: true }));
|
|
65
85
|
beforeEach(() => {
|
|
86
|
+
tunnels.borrowed.clear();
|
|
87
|
+
tunnels.borrowCalls = [];
|
|
88
|
+
tunnels.borrowError = undefined;
|
|
66
89
|
const real = typedi_1.Container.get.bind(typedi_1.Container);
|
|
67
90
|
sinon_1.default.stub(typedi_1.Container, 'get').callsFake((token) => {
|
|
68
91
|
if (token === IOSTunnels_1.IOSTunnels)
|
|
@@ -88,6 +111,20 @@ describe("go-ios commands find their own phone's tunnel (GO_IOS_AGENT_PORT)", ()
|
|
|
88
111
|
const png = yield new WDAClient_1.WDAClient().getScreenshot(NO_TUNNEL);
|
|
89
112
|
(0, chai_1.expect)(Buffer.from(png, 'base64').toString()).to.equal('port=');
|
|
90
113
|
}));
|
|
114
|
+
// On iOS 17+ the screenshot service is reached only through the tunnel,
|
|
115
|
+
// and a phone that isn't streaming has none.
|
|
116
|
+
it("opens the phone's tunnel for a screenshot when it has none", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
117
|
+
const png = yield new WDAClient_1.WDAClient().getScreenshot(NEEDS_TUNNEL);
|
|
118
|
+
(0, chai_1.expect)(tunnels.borrowCalls).to.deep.equal([NEEDS_TUNNEL]);
|
|
119
|
+
(0, chai_1.expect)(Buffer.from(png, 'base64').toString()).to.equal('port=12102');
|
|
120
|
+
}));
|
|
121
|
+
it("says why, when the tunnel can't start and WebDriverAgent can't answer either", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
122
|
+
tunnels.borrowError = new Error('The go-ios tunnel for X exited before it was ready (exit code 1): failed to start tunnel: EOF');
|
|
123
|
+
const client = new WDAClient_1.WDAClient();
|
|
124
|
+
sinon_1.default.stub(client, 'sendWDACommand').rejects(new Error('connect ECONNREFUSED'));
|
|
125
|
+
const err = yield client.getScreenshot(NEEDS_TUNNEL).then(() => null, (e) => e);
|
|
126
|
+
(0, chai_1.expect)(err === null || err === void 0 ? void 0 : err.message).to.match(/exited before it was ready .*failed to start tunnel: EOF/);
|
|
127
|
+
}));
|
|
91
128
|
});
|
|
92
129
|
describe('the logs read (WDAClient syslog)', () => {
|
|
93
130
|
let client;
|
|
@@ -198,6 +198,7 @@ describe('go-ios tunnels under a live Appium session (fake exec, fake processes)
|
|
|
198
198
|
envFor: sinon_1.default.stub().callsFake(() => (Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' }))),
|
|
199
199
|
stop: sinon_1.default.stub().resolves(),
|
|
200
200
|
touch: sinon_1.default.stub().resolves(),
|
|
201
|
+
isOnDemand: sinon_1.default.stub().returns(false),
|
|
201
202
|
};
|
|
202
203
|
svc = iosService();
|
|
203
204
|
const real = typedi_1.Container.get.bind(typedi_1.Container);
|
|
@@ -363,6 +364,15 @@ describe('go-ios tunnels under a live Appium session (fake exec, fake processes)
|
|
|
363
364
|
(0, chai_1.expect)(defaultPorts()).to.deep.equal([]);
|
|
364
365
|
}));
|
|
365
366
|
});
|
|
367
|
+
describe('a start on a phone whose tunnel was opened for screenshots', () => {
|
|
368
|
+
it('takes that tunnel over, neither stopping nor reaping it', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
369
|
+
Object.assign(device, { busy: false, session_id: null });
|
|
370
|
+
tunnels.isOnDemand.withArgs(IPHONE).returns(true);
|
|
371
|
+
yield svc.cleanupOrphanTunnels(IPHONE);
|
|
372
|
+
(0, chai_1.expect)(tunnels.stop.called, 'the tracked tunnel').to.equal(false);
|
|
373
|
+
(0, chai_1.expect)(swept(), 'no pgrep reap of its process').to.deep.equal([]);
|
|
374
|
+
}));
|
|
375
|
+
});
|
|
366
376
|
describe('boot', () => {
|
|
367
377
|
it('reaps every go-ios process, then drops every tunnel port lease', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
368
378
|
yield svc.reapOrphanTunnels();
|
|
@@ -108,6 +108,8 @@ describe('iOS stream port leases (fake processes, real sockets, scratch DB)', ()
|
|
|
108
108
|
let appiumWdaPorts;
|
|
109
109
|
let ios;
|
|
110
110
|
let android;
|
|
111
|
+
/** The stream's runwda exits at once, as when WebDriverAgent fails to launch. */
|
|
112
|
+
let runwdaExits;
|
|
111
113
|
let tunnels;
|
|
112
114
|
beforeEach(() => {
|
|
113
115
|
allocator = new PortAllocator_1.PortAllocator();
|
|
@@ -123,6 +125,7 @@ describe('iOS stream port leases (fake processes, real sockets, scratch DB)', ()
|
|
|
123
125
|
spawned = [];
|
|
124
126
|
extraServers = [];
|
|
125
127
|
appiumWdaPorts = new Set();
|
|
128
|
+
runwdaExits = false;
|
|
126
129
|
tunnels = {
|
|
127
130
|
ensure: sinon_1.default.stub().resolves(null),
|
|
128
131
|
envFor: sinon_1.default.stub().callsFake(() => (Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' }))),
|
|
@@ -150,6 +153,8 @@ describe('iOS stream port leases (fake processes, real sockets, scratch DB)', ()
|
|
|
150
153
|
server = listenNow(Number(args[args.length - 1].split(':')[0]));
|
|
151
154
|
}
|
|
152
155
|
const proc = new FakeProcess(command, args, server, opts);
|
|
156
|
+
if (runwdaExits && args[0] === 'runwda')
|
|
157
|
+
proc.exitCode = 1;
|
|
153
158
|
spawned.push(proc);
|
|
154
159
|
return proc;
|
|
155
160
|
}));
|
|
@@ -236,6 +241,27 @@ describe('iOS stream port leases (fake processes, real sockets, scratch DB)', ()
|
|
|
236
241
|
const runwda = spawned.find((p) => p.command === ios.goIOSPath && p.args[0] === 'runwda');
|
|
237
242
|
(0, chai_1.expect)((_c = (_b = runwda === null || runwda === void 0 ? void 0 : runwda.opts) === null || _b === void 0 ? void 0 : _b.env) === null || _c === void 0 ? void 0 : _c.GO_IOS_AGENT_PORT, "runwda finds this phone's tunnel").to.equal('12100');
|
|
238
243
|
}));
|
|
244
|
+
describe("a start whose WebDriverAgent fails after the iPhone's tunnel came up", () => {
|
|
245
|
+
beforeEach(() => {
|
|
246
|
+
tunnels.ensure.resolves(12100);
|
|
247
|
+
runwdaExits = true;
|
|
248
|
+
sinon_1.default.stub(ios, 'isGoIOSAvailable').resolves(true);
|
|
249
|
+
});
|
|
250
|
+
it('stops that tunnel, so it neither lingers nor holds its ports', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
251
|
+
var _a, _b;
|
|
252
|
+
const err = yield ios.startStream(IPHONE).then(() => null, (e) => e);
|
|
253
|
+
(0, chai_1.expect)(err === null || err === void 0 ? void 0 : err.message).to.match(/WDA process exited/);
|
|
254
|
+
(0, chai_1.expect)(tunnels.stop.calledWith(IPHONE)).to.equal(true);
|
|
255
|
+
(0, chai_1.expect)((_a = ios.getStreamStatus(IPHONE)) === null || _a === void 0 ? void 0 : _a.status).to.equal('error');
|
|
256
|
+
(0, chai_1.expect)((_b = ios.getStreamStatus(IPHONE)) === null || _b === void 0 ? void 0 : _b.tunnelPort).to.equal(null);
|
|
257
|
+
}));
|
|
258
|
+
it('leaves it running while an Appium session holds the iPhone', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
259
|
+
Object.assign(devices.get(IPHONE), { busy: true, session_id: '7f0c5a52-appium-session' });
|
|
260
|
+
const err = yield ios.startStream(IPHONE).then(() => null, (e) => e);
|
|
261
|
+
(0, chai_1.expect)(err === null || err === void 0 ? void 0 : err.message).to.match(/WDA process exited/);
|
|
262
|
+
(0, chai_1.expect)(tunnels.stop.called).to.equal(false);
|
|
263
|
+
}));
|
|
264
|
+
});
|
|
239
265
|
it('stopping a stale iOS session never deletes a lease another device holds', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
240
266
|
var _a;
|
|
241
267
|
// The S9+ has since been leased the port the stale iPhone session names.
|
|
@@ -80,10 +80,16 @@ class TestTunnels extends IOSTunnels_1.IOSTunnels {
|
|
|
80
80
|
/** Each tunnel-info poll: true is the phone's tunnel on P + 1, false a 404. */
|
|
81
81
|
this.answers = () => true;
|
|
82
82
|
this.ports = fakeAllocator();
|
|
83
|
+
this.clock = 1000000;
|
|
84
|
+
this.versionReads = 0;
|
|
83
85
|
this.nextPid = 7000;
|
|
84
86
|
}
|
|
87
|
+
now() {
|
|
88
|
+
return this.clock;
|
|
89
|
+
}
|
|
85
90
|
iosVersion(udid) {
|
|
86
91
|
return __awaiter(this, void 0, void 0, function* () {
|
|
92
|
+
this.versionReads += 1;
|
|
87
93
|
const version = this.versions.get(udid);
|
|
88
94
|
if (version === undefined)
|
|
89
95
|
throw new Error(`no such phone: ${udid}`);
|
|
@@ -181,6 +187,18 @@ describe('IOSTunnels: a go-ios tunnel per iPhone', () => {
|
|
|
181
187
|
(0, chai_1.expect)(t.portFor(PHONE_A)).to.equal(undefined);
|
|
182
188
|
(0, chai_1.expect)([...t.ports.leased.keys()]).to.deep.equal([]);
|
|
183
189
|
}));
|
|
190
|
+
it("says why the tunnel exited before it was ready, in go-ios's words", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
191
|
+
t.answers = () => {
|
|
192
|
+
const proc = t.spawned[0].proc;
|
|
193
|
+
proc.stderr.emit('data', Buffer.from('{"level":"INFO","msg":"Using userspace networking"}\n' +
|
|
194
|
+
'{"level":"ERROR","msg":"failed to start tunnel","error":"listen tcp 127.0.0.1:12101: bind: address already in use"}\n'));
|
|
195
|
+
proc.exit(1);
|
|
196
|
+
return false;
|
|
197
|
+
};
|
|
198
|
+
const err = yield t.ensure(PHONE_A).then(() => null, (e) => e);
|
|
199
|
+
(0, chai_1.expect)(err === null || err === void 0 ? void 0 : err.message).to.equal(`The go-ios tunnel for ${PHONE_A} exited before it was ready (exit code 1): ` +
|
|
200
|
+
'failed to start tunnel: listen tcp 127.0.0.1:12101: bind: address already in use');
|
|
201
|
+
}));
|
|
184
202
|
it('fails the start when the tunnel comes up on another traffic port, and stops it', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
185
203
|
t.answers = () => ({ state: 'up', userspacePort: 12102 });
|
|
186
204
|
const err = yield t.ensure(PHONE_A).then(() => null, (e) => e);
|
|
@@ -320,6 +338,78 @@ describe('IOSTunnels: a phone that goes away (go-ios keeps its agent running)',
|
|
|
320
338
|
}
|
|
321
339
|
}));
|
|
322
340
|
});
|
|
341
|
+
// A screenshot on iOS 17+ goes through the phone's tunnel, and a phone that
|
|
342
|
+
// isn't streaming has none: one is opened for screenshots and kept while they
|
|
343
|
+
// keep coming.
|
|
344
|
+
describe('IOSTunnels: a tunnel opened for screenshots', () => {
|
|
345
|
+
let t;
|
|
346
|
+
beforeEach(() => {
|
|
347
|
+
sinon_1.default.stub(process, 'kill');
|
|
348
|
+
t = new TestTunnels();
|
|
349
|
+
t.versions.set(PHONE_A, 17.4);
|
|
350
|
+
});
|
|
351
|
+
afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
352
|
+
yield t.stop(PHONE_A);
|
|
353
|
+
sinon_1.default.restore();
|
|
354
|
+
}));
|
|
355
|
+
it('opens one when the phone has none, and the next screenshot reuses it', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
356
|
+
(0, chai_1.expect)(yield t.borrow(PHONE_A)).to.equal(12100);
|
|
357
|
+
(0, chai_1.expect)(yield t.borrow(PHONE_A)).to.equal(12100);
|
|
358
|
+
(0, chai_1.expect)(t.spawned.length).to.equal(1);
|
|
359
|
+
(0, chai_1.expect)(t.envFor(PHONE_A).GO_IOS_AGENT_PORT).to.equal('12100');
|
|
360
|
+
}));
|
|
361
|
+
it('stops it 2 minutes after the last screenshot, and gives its ports back', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
362
|
+
yield t.borrow(PHONE_A);
|
|
363
|
+
t.clock += IOSTunnels_1.TUNNEL_BORROW_IDLE_MS - 1000;
|
|
364
|
+
yield t.borrow(PHONE_A); // another screenshot: the 2 minutes start again
|
|
365
|
+
t.clock += IOSTunnels_1.TUNNEL_BORROW_IDLE_MS - 1000;
|
|
366
|
+
yield t.checkTunnels();
|
|
367
|
+
(0, chai_1.expect)(t.portFor(PHONE_A), 'last used 119 s ago').to.equal(12100);
|
|
368
|
+
t.clock += 2000;
|
|
369
|
+
yield t.checkTunnels();
|
|
370
|
+
yield settle();
|
|
371
|
+
(0, chai_1.expect)(t.portFor(PHONE_A)).to.equal(undefined);
|
|
372
|
+
(0, chai_1.expect)(t.killed).to.deep.equal([t.spawned[0].proc.pid]);
|
|
373
|
+
(0, chai_1.expect)([...t.ports.leased.keys()]).to.deep.equal([]);
|
|
374
|
+
}));
|
|
375
|
+
it('never stops one a stream took over', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
376
|
+
yield t.borrow(PHONE_A);
|
|
377
|
+
(0, chai_1.expect)(t.isOnDemand(PHONE_A)).to.equal(true);
|
|
378
|
+
(0, chai_1.expect)(yield t.ensure(PHONE_A), 'the stream takes it').to.equal(12100);
|
|
379
|
+
(0, chai_1.expect)(t.isOnDemand(PHONE_A)).to.equal(false);
|
|
380
|
+
t.clock += 10 * IOSTunnels_1.TUNNEL_BORROW_IDLE_MS;
|
|
381
|
+
yield t.checkTunnels();
|
|
382
|
+
(0, chai_1.expect)(t.portFor(PHONE_A)).to.equal(12100);
|
|
383
|
+
(0, chai_1.expect)(t.spawned.length).to.equal(1);
|
|
384
|
+
}));
|
|
385
|
+
it("never stops a stream's own tunnel for being idle", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
386
|
+
yield t.ensure(PHONE_A);
|
|
387
|
+
yield t.borrow(PHONE_A); // a screenshot while the stream runs
|
|
388
|
+
t.clock += 10 * IOSTunnels_1.TUNNEL_BORROW_IDLE_MS;
|
|
389
|
+
yield t.checkTunnels();
|
|
390
|
+
(0, chai_1.expect)(t.portFor(PHONE_A)).to.equal(12100);
|
|
391
|
+
(0, chai_1.expect)(t.isOnDemand(PHONE_A)).to.equal(false);
|
|
392
|
+
}));
|
|
393
|
+
it('opens none for a phone below iOS 17, and reads its version once', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
394
|
+
t.versions.set('old-iphone', 16.7);
|
|
395
|
+
(0, chai_1.expect)(yield t.borrow('old-iphone')).to.equal(null);
|
|
396
|
+
(0, chai_1.expect)(yield t.borrow('old-iphone')).to.equal(null);
|
|
397
|
+
(0, chai_1.expect)(t.versionReads).to.equal(1);
|
|
398
|
+
(0, chai_1.expect)(t.spawned).to.deep.equal([]);
|
|
399
|
+
}));
|
|
400
|
+
});
|
|
401
|
+
describe('goIosReason', () => {
|
|
402
|
+
it("reads go-ios's last error or warning", () => {
|
|
403
|
+
(0, chai_1.expect)((0, IOSTunnels_1.goIosReason)('{"level":"INFO","msg":"Tunnel server started"}\n')).to.equal(undefined);
|
|
404
|
+
(0, chai_1.expect)((0, IOSTunnels_1.goIosReason)('{"level":"WARN","msg":"failed to get tunnel info","udid":"X"}')).to.equal('failed to get tunnel info');
|
|
405
|
+
(0, chai_1.expect)((0, IOSTunnels_1.goIosReason)('{"level":"ERROR","msg":"first"}\n{"level":"ERROR","msg":"failed to start tunnel","error":"EOF"}\n')).to.equal('failed to start tunnel: EOF');
|
|
406
|
+
});
|
|
407
|
+
it('keeps a line that is not JSON, trimmed and at most 200 characters', () => {
|
|
408
|
+
(0, chai_1.expect)((0, IOSTunnels_1.goIosReason)(' panic: runtime error \n')).to.equal('panic: runtime error');
|
|
409
|
+
(0, chai_1.expect)((0, IOSTunnels_1.goIosReason)('x'.repeat(500))).to.have.length(200);
|
|
410
|
+
(0, chai_1.expect)((0, IOSTunnels_1.goIosReason)('\n \n')).to.equal(undefined);
|
|
411
|
+
});
|
|
412
|
+
});
|
|
323
413
|
describe('iosVersionOf', () => {
|
|
324
414
|
it('reads the version ios info reports', () => {
|
|
325
415
|
(0, chai_1.expect)((0, IOSTunnels_1.iosVersionOf)({ ProductVersion: '17.2.1' })).to.equal(17.2);
|