@xenon-device-management/xenon 2.7.0 → 2.8.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-oISPBknJ.js → AnnotationOverlay-XmDGA9CJ.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-BOm-SKAJ.js → ApiKeyGate-BilRCzB-.js} +1 -1
- package/lib/public/assets/{BugReportButton-C1WMf_3A.js → BugReportButton-_buEV98z.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-BPJ5n5fY.js → DeviceMosaicView-DTEiC4gk.js} +1 -1
- package/lib/public/assets/{EmptyState-CZg80GnN.js → EmptyState-BhQfPUga.js} +1 -1
- package/lib/public/assets/{FieldGroup-D7zftz-q.js → FieldGroup-VPsU3LwN.js} +1 -1
- package/lib/public/assets/{FilterMenu-BKHKlX-8.js → FilterMenu-Br9o7dBS.js} +1 -1
- package/lib/public/assets/{Menu-Ccznqijj.js → Menu-dF_HlxcB.js} +1 -1
- package/lib/public/assets/{Modal-FxdOVK12.js → Modal-51305CEc.js} +1 -1
- package/lib/public/assets/{RecordingPage-BL8B1IKY.js → RecordingPage-BYQfiIEd.js} +1 -1
- package/lib/public/assets/{RecordingsPage-CCCL2xwa.js → RecordingsPage-C7A2ZMU_.js} +1 -1
- package/lib/public/assets/{SegmentedControl-B3EMys8L.js → SegmentedControl--_Zp_x-E.js} +1 -1
- package/lib/public/assets/{SettingCard-BwvdX-C6.js → SettingCard-DKlLEbu9.js} +1 -1
- package/lib/public/assets/{Table-sGNs22pM.js → Table-mknFnLyL.js} +1 -1
- package/lib/public/assets/{activity-rQQOy6SD.js → activity-D4657Amj.js} +1 -1
- package/lib/public/assets/{ai-settings-uIrII3el.js → ai-settings-_sXFJMDz.js} +1 -1
- package/lib/public/assets/{api-keys-B1-QmrZJ.js → api-keys-BV7AQW2N.js} +1 -1
- package/lib/public/assets/{apps-B8gYjYhN.js → apps-D-qPk58H.js} +1 -1
- package/lib/public/assets/{arrow-left-CLT_rU6o.js → arrow-left-B6xZI-lv.js} +1 -1
- package/lib/public/assets/{arrow-right-DqK8eHkB.js → arrow-right-KOusXQT_.js} +1 -1
- package/lib/public/assets/{arrow-up-right-D96XxKQY.js → arrow-up-right--y5SjylA.js} +1 -1
- package/lib/public/assets/{auth-shell-DpBFmfin.js → auth-shell-BOGfjiRi.js} +2 -2
- package/lib/public/assets/{builds-page-DqD7ySkz.js → builds-page-CgobTBVN.js} +1 -1
- package/lib/public/assets/{button-BrVVNvSB.js → button-Ci58y6Ou.js} +1 -1
- package/lib/public/assets/{calendar-FX_no0De.js → calendar-BYLnN-JG.js} +1 -1
- package/lib/public/assets/{check-C6RAxVq9.js → check-BZFRUKhB.js} +1 -1
- package/lib/public/assets/{chevron-left-CcTJUfpS.js → chevron-left-C-olFL5E.js} +1 -1
- package/lib/public/assets/{chevron-right-C39WUDQP.js → chevron-right-CmxzLyEE.js} +1 -1
- package/lib/public/assets/{circle-check-Fo1eEzZe.js → circle-check-Duvlh9lf.js} +1 -1
- package/lib/public/assets/{circle-x-CXy5XzIu.js → circle-x-DoQgpxKQ.js} +1 -1
- package/lib/public/assets/{clock-PBGPHMrI.js → clock-yvaEQMYo.js} +1 -1
- package/lib/public/assets/{copy-ZGa-scbM.js → copy-CIcmMH4s.js} +1 -1
- package/lib/public/assets/{device-explorer-BRYO7iUM.js → device-explorer-Cf-d-udL.js} +1 -1
- package/lib/public/assets/{download-vaODl7Bn.js → download-Ck75G4Sr.js} +1 -1
- package/lib/public/assets/{forgot-password-YfGbiIOM.js → forgot-password-DcOi08XK.js} +1 -1
- package/lib/public/assets/{funnel-C05BIqke.js → funnel-CafvAmsz.js} +1 -1
- package/lib/public/assets/{index-DdsuniRs.js → index-9Npot8Nw.js} +2 -2
- package/lib/public/assets/{index-1ST_YG3o.js → index-QT33_ARg.js} +1 -1
- package/lib/public/assets/{input-C6Zp2Ibb.js → input-DUA3Tk33.js} +1 -1
- package/lib/public/assets/{keycodes-BlkuYXfQ.js → keycodes-Dj17elyp.js} +1 -1
- package/lib/public/assets/{layers-B-3IDp3k.js → layers-5SyoCGaK.js} +1 -1
- package/lib/public/assets/{list-checks-BUUYV0Vf.js → list-checks-Blsk6Sts.js} +1 -1
- package/lib/public/assets/{lock-BUdNqIao.js → lock-DUyg9vcf.js} +1 -1
- package/lib/public/assets/{login-CD8bag9i.js → login-DWRTaaNU.js} +1 -1
- package/lib/public/assets/{maintenance-settings-BWQP_nfK.js → maintenance-settings-CwthiKtl.js} +1 -1
- package/lib/public/assets/{monitor-qfb5f9oQ.js → monitor-D7kOVM-I.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-DAAlUY2l.js → mouse-pointer-2-BzwITYs-.js} +1 -1
- package/lib/public/assets/{network-Qib4-4Yj.js → network-3c26VFLV.js} +1 -1
- package/lib/public/assets/{overview-BZQf20qe.js → overview-BamopQBx.js} +1 -1
- package/lib/public/assets/{page-header-4jNxdU1M.js → page-header-Cr568Bj2.js} +1 -1
- package/lib/public/assets/{plus-DVhPNSQy.js → plus-egFVGGUW.js} +1 -1
- package/lib/public/assets/{profile-page-DfITRDDC.js → profile-page-BetCnNKO.js} +1 -1
- package/lib/public/assets/{reset-password-s2xU05oa.js → reset-password-BgjuXPN-.js} +1 -1
- package/lib/public/assets/{runbook-page-DdEiXQcm.js → runbook-page-BbAREoE4.js} +1 -1
- package/lib/public/assets/{select-Bzs3ZUvb.js → select-BfZw_3u2.js} +1 -1
- package/lib/public/assets/{selector-detail-page-QvRRVchI.js → selector-detail-page-BqswNrpr.js} +1 -1
- package/lib/public/assets/{selector-health-DwUFxrZx.js → selector-health-Cbdio_9h.js} +1 -1
- package/lib/public/assets/{selector-health-page-N0tA9q9c.js → selector-health-page-qdM7e6Lx.js} +1 -1
- package/lib/public/assets/{session-detail-page-DcGGXXxL.js → session-detail-page-DFnhbs82.js} +1 -1
- package/lib/public/assets/{settings-BUqAjzeY.js → settings-Bg81ksOI.js} +1 -1
- package/lib/public/assets/{stat-tile-BwZr7q7_.js → stat-tile-CCR1O7iE.js} +1 -1
- package/lib/public/assets/{tablet-DiOf-kLM.js → tablet-DAHOR-iq.js} +1 -1
- package/lib/public/assets/{teams-DA6AfWGO.js → teams-Bz13DBCK.js} +1 -1
- package/lib/public/assets/{trash-2-BZuuog-n.js → trash-2-BgqAVxUr.js} +1 -1
- package/lib/public/assets/{upload-DLHV2k4Z.js → upload-Rvl2CCKn.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DRY4lLdd.js → use-password-reset-mode--PRWElm2.js} +1 -1
- package/lib/public/assets/{users-BlhcXeTG.js → users-BRkxjzNQ.js} +1 -1
- package/lib/public/assets/{users-Bd4LKBWY.js → users-O123SAYx.js} +1 -1
- package/lib/public/assets/{video-off-DIaDZGNl.js → video-off-DkNxxAWE.js} +1 -1
- package/lib/public/assets/{webhook-settings-IWvqbRk2.js → webhook-settings-D6UmShyI.js} +1 -1
- package/lib/public/assets/{zap-4Qy4hVbF.js → zap-D6UAiMKL.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/device-managers/ios/IOSLogStreamService.js +6 -3
- package/lib/src/device-managers/ios/IOSStreamService.js +38 -122
- package/lib/src/device-managers/ios/IOSTunnels.js +376 -0
- package/lib/src/device-managers/ios/WDAClient.js +5 -2
- package/lib/src/services/PortAllocator.js +65 -0
- package/lib/test/unit/ios-go-ios-tunnel-env.spec.js +154 -0
- package/lib/test/unit/ios-stream-appium-tunnel.spec.js +122 -32
- package/lib/test/unit/ios-stream-port-leases.spec.js +26 -5
- package/lib/test/unit/ios-tunnels.spec.js +387 -0
- package/lib/test/unit/port-allocator-pairs.spec.js +110 -0
- package/lib/test/unit/stream-own-device-row.spec.js +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
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;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
9
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
10
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
11
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
12
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
13
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
14
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
18
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
|
+
};
|
|
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;
|
|
22
|
+
exports.iosVersionOf = iosVersionOf;
|
|
23
|
+
/**
|
|
24
|
+
* One go-ios tunnel per iOS 17+ phone, each on its own leased pair of ports.
|
|
25
|
+
*
|
|
26
|
+
* On iOS 17 and later, the go-ios commands Xenon runs against a phone's
|
|
27
|
+
* services (`runwda`, `ostrace`, `syslog`, `screenshot`) reach it through a
|
|
28
|
+
* tunnel, which they find through that tunnel's info API. Through 2.7 every
|
|
29
|
+
* tunnel took go-ios's default ports (60105 for the API, 60106 for the
|
|
30
|
+
* traffic), so a second iPhone's stream killed the first one's tunnel.
|
|
31
|
+
*
|
|
32
|
+
* Each phone now gets `tunnel start --udid <phone> --tunnel-info-port <P>`,
|
|
33
|
+
* an isolated per-device agent: P from the `tunnel` port range, and P + 1,
|
|
34
|
+
* which go-ios derives for the phone's traffic. A command finds its phone's
|
|
35
|
+
* tunnel through GO_IOS_AGENT_PORT, set by {@link IOSTunnels.envFor}.
|
|
36
|
+
*
|
|
37
|
+
* go-ios does not end an agent when its phone is unplugged. The agent only
|
|
38
|
+
* drops the phone's tunnel, and on the replug starts a new one on its next
|
|
39
|
+
* traffic port, P + 2, then P + 3: the next phone's leased pair. So a tunnel
|
|
40
|
+
* whose phone went away, or whose traffic port moved, is stopped
|
|
41
|
+
* ({@link IOSTunnels.checkTunnels}, every few seconds), and the phone's next
|
|
42
|
+
* stream start makes a new one on a fresh pair.
|
|
43
|
+
*
|
|
44
|
+
* This is mechanism only. When a tunnel may be stopped (never under a live
|
|
45
|
+
* Appium session) is IOSStreamService's decision.
|
|
46
|
+
*/
|
|
47
|
+
const typedi_1 = require("typedi");
|
|
48
|
+
const child_process_1 = require("child_process");
|
|
49
|
+
const http_1 = __importDefault(require("http"));
|
|
50
|
+
const path_1 = __importDefault(require("path"));
|
|
51
|
+
const util_1 = require("util");
|
|
52
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
53
|
+
const helpers_1 = require("../../helpers");
|
|
54
|
+
const PortAllocator_1 = require("../../services/PortAllocator");
|
|
55
|
+
const ProcessRegistry_1 = require("../../services/ProcessRegistry");
|
|
56
|
+
const ResourceIsolationService_1 = require("../../services/ResourceIsolationService");
|
|
57
|
+
const iosStreamDiagnostics_1 = require("./iosStreamDiagnostics");
|
|
58
|
+
const tunnelProcess_1 = require("./tunnelProcess");
|
|
59
|
+
const execPromise = (0, util_1.promisify)(child_process_1.exec);
|
|
60
|
+
/** How long a new tunnel has to answer for its phone before the start goes on without it. */
|
|
61
|
+
exports.TUNNEL_READY_TIMEOUT_MS = 20000;
|
|
62
|
+
exports.TUNNEL_READY_POLL_MS = 500;
|
|
63
|
+
/**
|
|
64
|
+
* A tunnel's port lease: the same as the stream's own ports
|
|
65
|
+
* (IOSStreamService's STREAM_PORT_TTL_MS). The stream watchdog refreshes both
|
|
66
|
+
* every hour, the tunnel's through {@link IOSTunnels.touch}.
|
|
67
|
+
*/
|
|
68
|
+
exports.TUNNEL_LEASE_TTL_MS = 90 * 60 * 1000;
|
|
69
|
+
/** How often running tunnels are checked for a phone that went away. */
|
|
70
|
+
exports.TUNNEL_CHECK_MS = 5000;
|
|
71
|
+
/** The iOS version `ios info` reports, as a number: 17.2 for "17.2.1" or "iOS 17.2", 0 for none. */
|
|
72
|
+
function iosVersionOf(info) {
|
|
73
|
+
const versionStr = String(info.ProductVersion || info.HumanReadableProductVersionString || '0');
|
|
74
|
+
const match = versionStr.match(/(\d+\.?\d*)/);
|
|
75
|
+
return match ? parseFloat(match[0]) : 0;
|
|
76
|
+
}
|
|
77
|
+
/** An answer that puts the phone's traffic anywhere but its leased P + 1. */
|
|
78
|
+
function moved(info, port) {
|
|
79
|
+
return info.state === 'up' && info.userspacePort !== undefined && info.userspacePort !== port + 1;
|
|
80
|
+
}
|
|
81
|
+
let IOSTunnels = class IOSTunnels {
|
|
82
|
+
constructor() {
|
|
83
|
+
this.log = logger_1.default.scope('IOSTunnels');
|
|
84
|
+
this.tunnels = new Map();
|
|
85
|
+
this.checking = false;
|
|
86
|
+
this.goIOSPath = path_1.default.join((0, helpers_1.cachePath)('goIOS'), 'ios');
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The phone's tunnel-info port, starting its tunnel if it has none. Null
|
|
90
|
+
* for a phone below iOS 17, or whose version can't be read: it needs no
|
|
91
|
+
* tunnel. Throws when no pair of ports is left, or when the tunnel exits
|
|
92
|
+
* before it is ready.
|
|
93
|
+
*/
|
|
94
|
+
ensure(udid) {
|
|
95
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
96
|
+
var _a;
|
|
97
|
+
const running = this.portFor(udid);
|
|
98
|
+
if (running !== undefined)
|
|
99
|
+
return running;
|
|
100
|
+
let version;
|
|
101
|
+
try {
|
|
102
|
+
version = yield this.iosVersion(udid);
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
this.log.warn(`[${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}`);
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
if (version < 17)
|
|
109
|
+
return null;
|
|
110
|
+
const port = yield this.allocator().acquirePair('tunnel', udid, {
|
|
111
|
+
ttlMs: exports.TUNNEL_LEASE_TTL_MS,
|
|
112
|
+
});
|
|
113
|
+
let proc;
|
|
114
|
+
try {
|
|
115
|
+
proc = this.spawnTunnel(udid, [
|
|
116
|
+
'tunnel',
|
|
117
|
+
'start',
|
|
118
|
+
'--udid',
|
|
119
|
+
udid,
|
|
120
|
+
'--userspace',
|
|
121
|
+
'--tunnel-info-port',
|
|
122
|
+
String(port),
|
|
123
|
+
]);
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
yield this.releasePair(port, udid);
|
|
127
|
+
throw e;
|
|
128
|
+
}
|
|
129
|
+
const tunnel = { port, process: proc, ready: false };
|
|
130
|
+
this.tunnels.set(udid, tunnel);
|
|
131
|
+
this.watch();
|
|
132
|
+
// A tunnel whose process ends (go-ios crashed, or something killed it)
|
|
133
|
+
// gives its ports back, and the phone's next start gets a new one. An
|
|
134
|
+
// unplug doesn't end it: see checkTunnels.
|
|
135
|
+
const ended = () => {
|
|
136
|
+
if (this.tunnels.get(udid) !== tunnel)
|
|
137
|
+
return;
|
|
138
|
+
this.tunnels.delete(udid);
|
|
139
|
+
this.unwatchIfIdle();
|
|
140
|
+
void this.releasePair(port, udid);
|
|
141
|
+
};
|
|
142
|
+
proc.once('exit', ended);
|
|
143
|
+
proc.once('error', (err) => {
|
|
144
|
+
this.log.warn(`[${udid}] go-ios tunnel failed: ${err.message}`);
|
|
145
|
+
ended();
|
|
146
|
+
});
|
|
147
|
+
this.logOutput(udid, proc);
|
|
148
|
+
this.log.info(`[${udid}] iOS ${version}: starting a go-ios tunnel on ${port} (traffic on ${port + 1})`);
|
|
149
|
+
const polls = exports.TUNNEL_READY_TIMEOUT_MS / exports.TUNNEL_READY_POLL_MS;
|
|
150
|
+
for (let poll = 0; poll <= polls; poll++) {
|
|
151
|
+
if (poll > 0)
|
|
152
|
+
yield this.sleep(exports.TUNNEL_READY_POLL_MS);
|
|
153
|
+
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})`);
|
|
155
|
+
}
|
|
156
|
+
const info = yield this.tunnelInfo(port, udid);
|
|
157
|
+
if (moved(info, port)) {
|
|
158
|
+
// A first attempt failed and go-ios retried on its next port, which
|
|
159
|
+
// may be another phone's.
|
|
160
|
+
yield this.stop(udid);
|
|
161
|
+
throw new Error(`The go-ios tunnel for ${udid} came up on port ${info.userspacePort}, not its leased ${port + 1}, so it was stopped`);
|
|
162
|
+
}
|
|
163
|
+
if (info.state === 'up') {
|
|
164
|
+
tunnel.ready = true;
|
|
165
|
+
this.log.info(`[${udid}] go-ios tunnel ready on ${port}`);
|
|
166
|
+
return port;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
this.log.warn(`[${udid}] go-ios tunnel on ${port} not ready after ${exports.TUNNEL_READY_TIMEOUT_MS / 1000}s, proceeding anyway`);
|
|
170
|
+
return port;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/** The phone's tunnel-info port, while its tunnel runs. */
|
|
174
|
+
portFor(udid) {
|
|
175
|
+
const tunnel = this.tunnels.get(udid);
|
|
176
|
+
return tunnel && tunnel.process.exitCode === null ? tunnel.port : undefined;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The environment for a go-ios command about this phone: today's, plus
|
|
180
|
+
* GO_IOS_AGENT_PORT while the phone has a tunnel, so the command finds that
|
|
181
|
+
* tunnel and not go-ios's default 60105.
|
|
182
|
+
*/
|
|
183
|
+
envFor(udid) {
|
|
184
|
+
const env = Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' });
|
|
185
|
+
const port = this.portFor(udid);
|
|
186
|
+
if (port !== undefined)
|
|
187
|
+
env.GO_IOS_AGENT_PORT = String(port);
|
|
188
|
+
return env;
|
|
189
|
+
}
|
|
190
|
+
/** Stop the phone's tunnel and give its ports back. Other phones' tunnels are untouched. */
|
|
191
|
+
stop(udid) {
|
|
192
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
193
|
+
const tunnel = this.tunnels.get(udid);
|
|
194
|
+
if (!tunnel)
|
|
195
|
+
return;
|
|
196
|
+
this.tunnels.delete(udid);
|
|
197
|
+
this.unwatchIfIdle();
|
|
198
|
+
this.killGroup(tunnel.process.pid);
|
|
199
|
+
yield this.releasePair(tunnel.port, udid);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Stop each tunnel whose phone has gone, and give its ports back. go-ios
|
|
204
|
+
* keeps a per-device agent running when its phone is unplugged and, on the
|
|
205
|
+
* replug, starts the tunnel on its next traffic port: the next phone's
|
|
206
|
+
* pair. So a tunnel that had its phone and now answers 404, or that answers
|
|
207
|
+
* on a traffic port other than P + 1, is stopped. Either way the phone's
|
|
208
|
+
* stream has already lost it, and its next start makes a new one.
|
|
209
|
+
*
|
|
210
|
+
* An agent that doesn't answer is left alone: a busy agent isn't a gone
|
|
211
|
+
* phone, and one whose process died is handled by its exit.
|
|
212
|
+
*/
|
|
213
|
+
checkTunnels() {
|
|
214
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
215
|
+
if (this.checking)
|
|
216
|
+
return;
|
|
217
|
+
this.checking = true;
|
|
218
|
+
try {
|
|
219
|
+
for (const [udid, tunnel] of [...this.tunnels]) {
|
|
220
|
+
const info = yield this.tunnelInfo(tunnel.port, udid);
|
|
221
|
+
if (this.tunnels.get(udid) !== tunnel)
|
|
222
|
+
continue;
|
|
223
|
+
if (moved(info, tunnel.port)) {
|
|
224
|
+
this.log.warn(`[${udid}] go-ios moved the phone's tunnel from port ${tunnel.port + 1} to ${info.userspacePort} (it reconnected); stopping it`);
|
|
225
|
+
}
|
|
226
|
+
else if (info.state === 'gone' && tunnel.ready) {
|
|
227
|
+
this.log.warn(`[${udid}] go-ios tunnel on ${tunnel.port} lost its phone (unplugged?); stopping it and freeing its ports`);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
if (info.state === 'up')
|
|
231
|
+
tunnel.ready = true;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
yield this.stop(udid);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
finally {
|
|
238
|
+
this.checking = false;
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
watch() {
|
|
243
|
+
var _a, _b;
|
|
244
|
+
if (this.watcher)
|
|
245
|
+
return;
|
|
246
|
+
this.watcher = setInterval(() => void this.checkTunnels(), exports.TUNNEL_CHECK_MS);
|
|
247
|
+
(_b = (_a = this.watcher).unref) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
248
|
+
}
|
|
249
|
+
unwatchIfIdle() {
|
|
250
|
+
if (this.tunnels.size > 0 || !this.watcher)
|
|
251
|
+
return;
|
|
252
|
+
clearInterval(this.watcher);
|
|
253
|
+
this.watcher = undefined;
|
|
254
|
+
}
|
|
255
|
+
/** Keep the phone's tunnel ports leased. The stream watchdog calls this hourly. */
|
|
256
|
+
touch(udid) {
|
|
257
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
258
|
+
const tunnel = this.tunnels.get(udid);
|
|
259
|
+
if (!tunnel)
|
|
260
|
+
return;
|
|
261
|
+
const allocator = this.allocator();
|
|
262
|
+
yield allocator.touch(tunnel.port, exports.TUNNEL_LEASE_TTL_MS);
|
|
263
|
+
yield allocator.touch(tunnel.port + 1, exports.TUNNEL_LEASE_TTL_MS);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
releasePair(port, udid) {
|
|
267
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
268
|
+
var _a;
|
|
269
|
+
try {
|
|
270
|
+
const allocator = this.allocator();
|
|
271
|
+
yield allocator.release(port, udid);
|
|
272
|
+
yield allocator.release(port + 1, udid);
|
|
273
|
+
}
|
|
274
|
+
catch (e) {
|
|
275
|
+
this.log.warn(`[${udid}] Could not release tunnel ports ${port}-${port + 1}: ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* 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.
|
|
282
|
+
*/
|
|
283
|
+
logOutput(udid, proc) {
|
|
284
|
+
var _a, _b;
|
|
285
|
+
(_a = proc.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => this.log.debug(`Tunnel [${udid}]: ${data}`));
|
|
286
|
+
const loggedUnsupported = new Set();
|
|
287
|
+
(_b = proc.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => {
|
|
288
|
+
const text = String(data);
|
|
289
|
+
const { unsupported, udid: targetUdid } = (0, iosStreamDiagnostics_1.classifyTunnelStderr)(text);
|
|
290
|
+
if (unsupported) {
|
|
291
|
+
const key = targetUdid !== null && targetUdid !== void 0 ? targetUdid : 'unknown';
|
|
292
|
+
if (!loggedUnsupported.has(key)) {
|
|
293
|
+
loggedUnsupported.add(key);
|
|
294
|
+
this.log.debug(`Tunnel [${udid}]: device ${key} is pre-iOS 17 and needs no tunnel; suppressing repeated go-ios warnings`);
|
|
295
|
+
}
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
this.log.debug(`Tunnel Err [${udid}]: ${text}`);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
// ---------------------------------------------------------------------
|
|
302
|
+
// Seams. Overridden by tests; the defaults are the real thing.
|
|
303
|
+
// ---------------------------------------------------------------------
|
|
304
|
+
iosVersion(udid) {
|
|
305
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
306
|
+
const { stdout } = yield execPromise(`"${this.goIOSPath}" info --udid ${udid}`, {
|
|
307
|
+
env: Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' }),
|
|
308
|
+
});
|
|
309
|
+
return iosVersionOf(JSON.parse(stdout));
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Detached, so the tunnel leads its own process group and its self-forking
|
|
314
|
+
* agent children die with it (see ./tunnelProcess).
|
|
315
|
+
*/
|
|
316
|
+
spawnTunnel(udid, args) {
|
|
317
|
+
const wrapped = typedi_1.Container.get(ResourceIsolationService_1.ResourceIsolationService).wrapSpawn(this.goIOSPath, args, 'Performance');
|
|
318
|
+
const proc = (0, child_process_1.spawn)(wrapped.command, wrapped.args, (0, tunnelProcess_1.tunnelSpawnOptions)(Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' })));
|
|
319
|
+
typedi_1.Container.get(ProcessRegistry_1.ProcessRegistry).track({ kind: 'other', udid, process: proc });
|
|
320
|
+
return proc;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* What the tunnel's agent on `port` says about the phone. go-ios answers
|
|
324
|
+
* `GET /tunnel/<udid>` with 404 until it has the phone's tunnel, then 200
|
|
325
|
+
* with the tunnel, `userspaceTunPort` included.
|
|
326
|
+
*/
|
|
327
|
+
tunnelInfo(port, udid) {
|
|
328
|
+
return new Promise((resolve) => {
|
|
329
|
+
const req = http_1.default.get({
|
|
330
|
+
host: '127.0.0.1',
|
|
331
|
+
port,
|
|
332
|
+
path: `/tunnel/${encodeURIComponent(udid)}`,
|
|
333
|
+
timeout: 1000,
|
|
334
|
+
agent: false,
|
|
335
|
+
}, (res) => {
|
|
336
|
+
if (res.statusCode !== 200) {
|
|
337
|
+
res.resume();
|
|
338
|
+
resolve(res.statusCode === 404 ? { state: 'gone' } : { state: 'unknown' });
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
let body = '';
|
|
342
|
+
res.setEncoding('utf8');
|
|
343
|
+
res.on('data', (chunk) => (body += chunk));
|
|
344
|
+
res.on('end', () => {
|
|
345
|
+
var _a;
|
|
346
|
+
let userspacePort;
|
|
347
|
+
try {
|
|
348
|
+
const value = (_a = JSON.parse(body)) === null || _a === void 0 ? void 0 : _a.userspaceTunPort;
|
|
349
|
+
if (typeof value === 'number')
|
|
350
|
+
userspacePort = value;
|
|
351
|
+
}
|
|
352
|
+
catch (_b) {
|
|
353
|
+
// A 200 without JSON still says the tunnel is up.
|
|
354
|
+
}
|
|
355
|
+
resolve({ state: 'up', userspacePort });
|
|
356
|
+
});
|
|
357
|
+
res.on('error', () => resolve({ state: 'unknown' }));
|
|
358
|
+
});
|
|
359
|
+
req.on('timeout', () => req.destroy());
|
|
360
|
+
req.on('error', () => resolve({ state: 'unknown' }));
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
sleep(ms) {
|
|
364
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
365
|
+
}
|
|
366
|
+
killGroup(pid) {
|
|
367
|
+
(0, tunnelProcess_1.killProcessGroup)(pid);
|
|
368
|
+
}
|
|
369
|
+
allocator() {
|
|
370
|
+
return typedi_1.Container.get(PortAllocator_1.PortAllocator);
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
exports.IOSTunnels = IOSTunnels;
|
|
374
|
+
exports.IOSTunnels = IOSTunnels = __decorate([
|
|
375
|
+
(0, typedi_1.Service)()
|
|
376
|
+
], IOSTunnels);
|
|
@@ -26,6 +26,7 @@ const logger_1 = __importDefault(require("../../logger"));
|
|
|
26
26
|
const typedi_1 = require("typedi");
|
|
27
27
|
const ProcessRegistry_1 = require("../../services/ProcessRegistry");
|
|
28
28
|
const IOSStreamService_1 = __importDefault(require("./IOSStreamService"));
|
|
29
|
+
const IOSTunnels_1 = require("./IOSTunnels");
|
|
29
30
|
const device_store_1 = require("../../data-service/device-store");
|
|
30
31
|
const child_process_1 = require("child_process");
|
|
31
32
|
const util_1 = require("util");
|
|
@@ -398,7 +399,8 @@ let WDAClient = WDAClient_1 = class WDAClient {
|
|
|
398
399
|
try {
|
|
399
400
|
const p = path_1.default.join(os_1.default.tmpdir(), `screenshot-${udid}.png`);
|
|
400
401
|
yield execFilePromise(s.goIOSPath, ['screenshot', '--udid', udid, '--output', p], {
|
|
401
|
-
|
|
402
|
+
// This phone's own go-ios tunnel on iOS 17+ (see IOSTunnels).
|
|
403
|
+
env: typedi_1.Container.get(IOSTunnels_1.IOSTunnels).envFor(udid),
|
|
402
404
|
});
|
|
403
405
|
const b = yield fs_extra_1.default.readFile(p);
|
|
404
406
|
yield fs_extra_1.default.remove(p);
|
|
@@ -763,7 +765,8 @@ let WDAClient = WDAClient_1 = class WDAClient {
|
|
|
763
765
|
}
|
|
764
766
|
this.log.info(`[LogStream] Starting persistent syslog for ${udid}: ${command} ${args.join(' ')}`);
|
|
765
767
|
const proc = (0, child_process_1.spawn)(command, args, {
|
|
766
|
-
|
|
768
|
+
// This phone's own go-ios tunnel on iOS 17+ (see IOSTunnels).
|
|
769
|
+
env: typedi_1.Container.get(IOSTunnels_1.IOSTunnels).envFor(udid),
|
|
767
770
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
768
771
|
});
|
|
769
772
|
typedi_1.Container.get(ProcessRegistry_1.ProcessRegistry).track({ kind: 'log-tailer', udid, process: proc });
|
|
@@ -31,6 +31,7 @@ const DEFAULT_RANGES = {
|
|
|
31
31
|
mjpeg: [9100, 9199],
|
|
32
32
|
system: [10100, 10199],
|
|
33
33
|
proxy: [11100, 11199],
|
|
34
|
+
tunnel: [12100, 12199],
|
|
34
35
|
};
|
|
35
36
|
/** Loopback and wildcard, in both families. See isOsFree. */
|
|
36
37
|
const PROBE_HOSTS = ['127.0.0.1', '::1', '0.0.0.0', '::'];
|
|
@@ -145,6 +146,70 @@ let PortAllocator = class PortAllocator {
|
|
|
145
146
|
}
|
|
146
147
|
});
|
|
147
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Lease two adjacent ports, an even P and P + 1, and return P. A go-ios
|
|
151
|
+
* tunnel needs both: P for its tunnel-info API, and P + 1, which go-ios
|
|
152
|
+
* itself picks for the phone's traffic.
|
|
153
|
+
*
|
|
154
|
+
* Both must be free in the lease table, whatever purpose holds them, and at
|
|
155
|
+
* the OS. A half-taken pair is given back before the next one is tried.
|
|
156
|
+
*/
|
|
157
|
+
acquirePair(purpose_1, udid_1) {
|
|
158
|
+
return __awaiter(this, arguments, void 0, function* (purpose, udid, opts = {}) {
|
|
159
|
+
var _a;
|
|
160
|
+
const [start, end] = this.ranges[purpose];
|
|
161
|
+
const ttlMs = (_a = opts.ttlMs) !== null && _a !== void 0 ? _a : 60 * 60 * 1000;
|
|
162
|
+
const now = Date.now();
|
|
163
|
+
yield prisma_1.prisma.portLease.deleteMany({ where: { expiresAt: { lt: now } } });
|
|
164
|
+
const active = yield prisma_1.prisma.portLease.findMany({
|
|
165
|
+
where: { port: { gte: start, lte: end } },
|
|
166
|
+
select: { port: true },
|
|
167
|
+
});
|
|
168
|
+
const taken = new Set(active.map((l) => l.port));
|
|
169
|
+
const lease = {
|
|
170
|
+
purpose,
|
|
171
|
+
leasedToUdid: udid,
|
|
172
|
+
leasedToPid: opts.pid,
|
|
173
|
+
leasedAt: now,
|
|
174
|
+
expiresAt: now + ttlMs,
|
|
175
|
+
};
|
|
176
|
+
for (let port = start + (start % 2); port + 1 <= end; port += 2) {
|
|
177
|
+
if (taken.has(port) || taken.has(port + 1))
|
|
178
|
+
continue;
|
|
179
|
+
const leased = [];
|
|
180
|
+
try {
|
|
181
|
+
for (const p of [port, port + 1]) {
|
|
182
|
+
yield prisma_1.prisma.portLease.create({ data: Object.assign({ port: p }, lease) });
|
|
183
|
+
leased.push(p);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
yield this.releaseAll(leased, udid);
|
|
188
|
+
if (err.code === 'P2002')
|
|
189
|
+
continue;
|
|
190
|
+
throw err;
|
|
191
|
+
}
|
|
192
|
+
if ((yield this.isOsFree(port)) && (yield this.isOsFree(port + 1))) {
|
|
193
|
+
this.log.debug(`Leased ports ${port}-${port + 1} (${purpose}) to ${udid}`);
|
|
194
|
+
return port;
|
|
195
|
+
}
|
|
196
|
+
yield this.releaseAll(leased, udid);
|
|
197
|
+
}
|
|
198
|
+
throw new PortRangeExhaustedError(purpose);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
/** Delete every lease of `purpose`. At boot, when nothing of this server holds one. */
|
|
202
|
+
releasePurpose(purpose) {
|
|
203
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
204
|
+
yield prisma_1.prisma.portLease.deleteMany({ where: { purpose } });
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
releaseAll(ports, udid) {
|
|
208
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
209
|
+
for (const port of ports)
|
|
210
|
+
yield this.release(port, udid);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
148
213
|
/**
|
|
149
214
|
* Soft-block a port so the next `acquire` for the same purpose skips it.
|
|
150
215
|
* Used when bind fails (EADDRINUSE) even though our lease owned the number —
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const events_1 = require("events");
|
|
18
|
+
const fs_1 = __importDefault(require("fs"));
|
|
19
|
+
const os_1 = __importDefault(require("os"));
|
|
20
|
+
const path_1 = __importDefault(require("path"));
|
|
21
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
22
|
+
const typedi_1 = require("typedi");
|
|
23
|
+
const IOSStreamService_1 = __importDefault(require("../../src/device-managers/ios/IOSStreamService"));
|
|
24
|
+
const IOSLogStreamService_1 = require("../../src/device-managers/ios/IOSLogStreamService");
|
|
25
|
+
const IOSTunnels_1 = require("../../src/device-managers/ios/IOSTunnels");
|
|
26
|
+
const WDAClient_1 = require("../../src/device-managers/ios/WDAClient");
|
|
27
|
+
const device_store_1 = require("../../src/data-service/device-store");
|
|
28
|
+
const ProcessRegistry_1 = require("../../src/services/ProcessRegistry");
|
|
29
|
+
/**
|
|
30
|
+
* On iOS 17+ a go-ios command finds its phone's tunnel through
|
|
31
|
+
* GO_IOS_AGENT_PORT. Each phone has its own tunnel now, so each command has
|
|
32
|
+
* to carry its own phone's port, or it reaches go-ios's default 60105, where
|
|
33
|
+
* no tunnel listens.
|
|
34
|
+
*
|
|
35
|
+
* `go-ios` here is a shell script that reports the port it was run with.
|
|
36
|
+
*/
|
|
37
|
+
const WITH_TUNNEL = 'test-iphone-tunnel-00008150';
|
|
38
|
+
const NO_TUNNEL = 'test-iphone-plain-00008110';
|
|
39
|
+
const FAKE_GO_IOS = `#!/bin/sh
|
|
40
|
+
# Stands in for go-ios: reports the GO_IOS_AGENT_PORT it was run with.
|
|
41
|
+
case "$1" in
|
|
42
|
+
screenshot) printf 'port=%s' "$GO_IOS_AGENT_PORT" > "$5" ;;
|
|
43
|
+
syslog) printf '{"msg":"port=%s"}\\n' "$GO_IOS_AGENT_PORT"; exec sleep 5 ;;
|
|
44
|
+
esac
|
|
45
|
+
`;
|
|
46
|
+
/** IOSTunnels as if WITH_TUNNEL's tunnel ran on 12100 and NO_TUNNEL had none. */
|
|
47
|
+
const tunnels = {
|
|
48
|
+
envFor(udid) {
|
|
49
|
+
const env = Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' });
|
|
50
|
+
delete env.GO_IOS_AGENT_PORT;
|
|
51
|
+
if (udid === WITH_TUNNEL)
|
|
52
|
+
env.GO_IOS_AGENT_PORT = '12100';
|
|
53
|
+
return env;
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
describe("go-ios commands find their own phone's tunnel (GO_IOS_AGENT_PORT)", () => {
|
|
57
|
+
let dir;
|
|
58
|
+
let goIOS;
|
|
59
|
+
before(() => {
|
|
60
|
+
dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-fake-goios-'));
|
|
61
|
+
goIOS = path_1.default.join(dir, 'ios');
|
|
62
|
+
fs_1.default.writeFileSync(goIOS, FAKE_GO_IOS, { mode: 0o755 });
|
|
63
|
+
});
|
|
64
|
+
after(() => fs_1.default.rmSync(dir, { recursive: true, force: true }));
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
const real = typedi_1.Container.get.bind(typedi_1.Container);
|
|
67
|
+
sinon_1.default.stub(typedi_1.Container, 'get').callsFake((token) => {
|
|
68
|
+
if (token === IOSTunnels_1.IOSTunnels)
|
|
69
|
+
return tunnels;
|
|
70
|
+
if (token === IOSStreamService_1.default) {
|
|
71
|
+
return { goIOSPath: goIOS, isGoIOSAvailable: () => __awaiter(void 0, void 0, void 0, function* () { return true; }) };
|
|
72
|
+
}
|
|
73
|
+
if (token === ProcessRegistry_1.ProcessRegistry)
|
|
74
|
+
return { track: () => 'tracked' };
|
|
75
|
+
return real(token);
|
|
76
|
+
});
|
|
77
|
+
sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({
|
|
78
|
+
findDevice: (_a) => __awaiter(void 0, [_a], void 0, function* ({ udid }) { return ({ udid, realDevice: true }); }),
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
afterEach(() => sinon_1.default.restore());
|
|
82
|
+
describe("device control's screenshot (WDAClient)", () => {
|
|
83
|
+
it("runs go-ios screenshot with the phone's tunnel port", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
84
|
+
const png = yield new WDAClient_1.WDAClient().getScreenshot(WITH_TUNNEL);
|
|
85
|
+
(0, chai_1.expect)(Buffer.from(png, 'base64').toString()).to.equal('port=12100');
|
|
86
|
+
}));
|
|
87
|
+
it('runs it with no tunnel port for a phone that has no tunnel', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
88
|
+
const png = yield new WDAClient_1.WDAClient().getScreenshot(NO_TUNNEL);
|
|
89
|
+
(0, chai_1.expect)(Buffer.from(png, 'base64').toString()).to.equal('port=');
|
|
90
|
+
}));
|
|
91
|
+
});
|
|
92
|
+
describe('the logs read (WDAClient syslog)', () => {
|
|
93
|
+
let client;
|
|
94
|
+
beforeEach(() => {
|
|
95
|
+
client = new WDAClient_1.WDAClient();
|
|
96
|
+
});
|
|
97
|
+
afterEach(() => {
|
|
98
|
+
client.stopLogStream(WITH_TUNNEL);
|
|
99
|
+
client.stopLogStream(NO_TUNNEL);
|
|
100
|
+
});
|
|
101
|
+
/** The first line the phone's syslog yields; the first read starts it. */
|
|
102
|
+
function firstLine(udid) {
|
|
103
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
104
|
+
for (let i = 0; i < 50; i++) {
|
|
105
|
+
const lines = yield client.getLogs(udid);
|
|
106
|
+
if (lines)
|
|
107
|
+
return lines;
|
|
108
|
+
yield new Promise((resolve) => setTimeout(resolve, 100));
|
|
109
|
+
}
|
|
110
|
+
return '';
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
it("runs go-ios syslog with the phone's tunnel port", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
114
|
+
(0, chai_1.expect)(yield firstLine(WITH_TUNNEL)).to.equal('port=12100');
|
|
115
|
+
}));
|
|
116
|
+
it('runs it with no tunnel port for a phone that has no tunnel', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
117
|
+
(0, chai_1.expect)(yield firstLine(NO_TUNNEL)).to.equal('port=');
|
|
118
|
+
}));
|
|
119
|
+
});
|
|
120
|
+
describe('the live logs pane (IOSLogStreamService ostrace)', () => {
|
|
121
|
+
class CapturingLogs extends IOSLogStreamService_1.IOSLogStreamService {
|
|
122
|
+
constructor() {
|
|
123
|
+
super(...arguments);
|
|
124
|
+
this.spawned = [];
|
|
125
|
+
}
|
|
126
|
+
goIOSPath() {
|
|
127
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
128
|
+
return goIOS;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
spawnProcess(command, args, env) {
|
|
132
|
+
this.spawned.push({ command, args, env });
|
|
133
|
+
const proc = new events_1.EventEmitter();
|
|
134
|
+
proc.stdout = new events_1.EventEmitter();
|
|
135
|
+
proc.stderr = new events_1.EventEmitter();
|
|
136
|
+
return proc;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
it("runs go-ios ostrace with the phone's tunnel port", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
140
|
+
var _a;
|
|
141
|
+
const logs = new CapturingLogs();
|
|
142
|
+
yield logs.spawnOstrace(WITH_TUNNEL, {});
|
|
143
|
+
(0, chai_1.expect)(logs.spawned[0].args[0]).to.equal('ostrace');
|
|
144
|
+
(0, chai_1.expect)((_a = logs.spawned[0].env) === null || _a === void 0 ? void 0 : _a.GO_IOS_AGENT_PORT).to.equal('12100');
|
|
145
|
+
}));
|
|
146
|
+
it('runs it with no tunnel port for a phone that has no tunnel', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
147
|
+
var _a;
|
|
148
|
+
const logs = new CapturingLogs();
|
|
149
|
+
yield logs.spawnOstrace(NO_TUNNEL, {});
|
|
150
|
+
(0, chai_1.expect)(logs.spawned[0].env, 'an environment is passed').to.not.equal(undefined);
|
|
151
|
+
(0, chai_1.expect)((_a = logs.spawned[0].env) === null || _a === void 0 ? void 0 : _a.GO_IOS_AGENT_PORT).to.equal(undefined);
|
|
152
|
+
}));
|
|
153
|
+
});
|
|
154
|
+
});
|