stim 1.8.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -155
- package/dist/{android-DWssqHQ8.mjs → android-787HhP5w.mjs} +168 -39
- package/dist/{android-BRgBYzU0.mjs → android-DBkeHwpm.mjs} +250 -200
- package/dist/{android-DeYdjjBP.mjs → android-DPov-fv2.mjs} +2 -2
- package/dist/{android-cas-SWIjzeuL.mjs → android-cas-CITmIeWY.mjs} +22 -14
- package/dist/android-cas-compiler.mjs +2 -2
- package/dist/app-install-CrJ6t_bv.mjs +819 -0
- package/dist/budget-BP4jZZL4.mjs +620 -0
- package/dist/build-progress-BXx-ZBe4.mjs +512 -0
- package/dist/{cache-manifest-CYJi8-Hx.mjs → cache-manifest-Pubfp8wV.mjs} +4 -4
- package/dist/cache-manifest.mjs +1 -1
- package/dist/cli.mjs +22 -19
- package/dist/collector-run.d.mts +7 -1
- package/dist/collector-run.mjs +10 -9
- package/dist/{command-output-B3ycpsYv.mjs → command-output-D5z2_bcF.mjs} +11 -2
- package/dist/{config-B7iyKWkL.mjs → config-BEq0Qw_a.mjs} +134 -121
- package/dist/{dependency-state-D0M7oKA7.mjs → dependency-state-r9UesA9a.mjs} +2 -2
- package/dist/{deps-oCXVNEqQ.mjs → deps-DP4eWqea.mjs} +8 -5
- package/dist/{detached-entry-Bpo6ZY_I.mjs → detached-entry-DJHoca49.mjs} +1 -1
- package/dist/{device-BUoZyMoR.mjs → dev-client-92U593KT.mjs} +1090 -660
- package/dist/{device-RdujjHLB.mjs → device-DhTZe7L4.mjs} +13 -20
- package/dist/{build-lock-CgtsB6TV.mjs → device-lease-CbEaIk7b.mjs} +481 -27
- package/dist/{device-pool-BxRwdufO.mjs → device-pool-Di8Q-9Ro.mjs} +7 -7
- package/dist/{device-remote-ByN8P6CB.mjs → device-remote-Dy1jGj3U.mjs} +645 -119
- package/dist/{doctor-BMt156qe.mjs → doctor-B-inAJmt.mjs} +137 -113
- package/dist/{error-diagnostics-D1Kaf-eT.mjs → error-diagnostics-xxWKNO3-.mjs} +22 -17
- package/dist/exec-DK8KaVHK.mjs +133 -0
- package/dist/{gc-v2YcoJvm.mjs → gc-ChPFZMRC.mjs} +836 -813
- package/dist/{guide-BTAlD1_y.mjs → guide-DwmaD_Wa.mjs} +953 -209
- package/dist/guide-status-CWvti2Lg.mjs +125 -0
- package/dist/idle-DpRLp5lj.mjs +157 -0
- package/dist/{build-slots-C-oAHRT1.mjs → in-use-CjUluqIX.mjs} +102 -8
- package/dist/{ios-BcCiI4TS.mjs → ios-B2Fpqvlb.mjs} +60 -71
- package/dist/{ios-BP04pY5f.mjs → ios-Dd-hYJBh.mjs} +241 -234
- package/dist/{ios-device-CFXA46ez.mjs → ios-device-BiOsSVzd.mjs} +3 -2
- package/dist/{ios-device-CauFJiYx.mjs → ios-device-CtRdEVjx.mjs} +149 -52
- package/dist/launch-verify-CQS_Kg-C.mjs +365 -0
- package/dist/{logs-CakdBTtm.mjs → logs-MARIy6m9.mjs} +20 -15
- package/dist/metro-BWLOJ_Wr.mjs +409 -0
- package/dist/{named-ports-DzMOlmaX.mjs → named-ports-DiTfSz6p.mjs} +4 -4
- package/dist/{ndjson-BZwI1NBv.mjs → ndjson-CDY9IkVC.mjs} +23 -43
- package/dist/{ndjson-DvHSsNU9.d.mts → ndjson-Du3j0AQ9.d.mts} +1 -0
- package/dist/{ownership-Cp52QAK1.mjs → ownership-D2xbLVoz.mjs} +2 -3
- package/dist/{teardown-BJqEzPo_.mjs → ownership-DZjdx8Gq.mjs} +664 -612
- package/dist/{ports-BIbJaKNW.mjs → ports-CLSSFOuz.mjs} +3 -3
- package/dist/{process-output-Dl5uKE03.mjs → process-output-Fpl8PYi8.mjs} +1 -1
- package/dist/{project-DDZWf_16.mjs → project-BXZzOABt.mjs} +8 -3
- package/dist/{reload-BdypPbjM.mjs → reload-DoETJ3rl.mjs} +11 -13
- package/dist/{server-bare-BQkyxa_b.mjs → server-bare-DVbb7kXH.mjs} +2 -2
- package/dist/{server-expo-BVAA1OqJ.mjs → server-expo-BvOCeakp.mjs} +8 -7
- package/dist/server-expo-DBVTdult.mjs +2 -0
- package/dist/{settings-BXXorVM2.mjs → settings-DOugPlPz.mjs} +59 -159
- package/dist/settings-EWYNwe72.mjs +378 -0
- package/dist/settings.schema.json +1064 -0
- package/dist/simslim-Ca3487H6.mjs +436 -0
- package/dist/{slot-launch-ZUUe0TQP.mjs → slot-launch-C5cg0fmk.mjs} +4 -4
- package/dist/spawn-claims-CUleC2cM.mjs +49 -0
- package/dist/{start-CmxtOKjU.mjs → start-F9gv5dcC.mjs} +159 -114
- package/dist/start-VoFsPSes.mjs +2 -0
- package/dist/{state-DZobzxLW.mjs → state-WGdIbi_d.mjs} +5 -3
- package/dist/{stats-BYcZ6n1h.mjs → stats-BAgJaLA4.mjs} +66 -5
- package/dist/{stats-CLg5RuDS.mjs → stats-DBYJ5bxL.mjs} +4 -4
- package/dist/status-DLCD-o3s.mjs +764 -0
- package/dist/status-Dr4fRkym.mjs +216 -0
- package/dist/{guide-status-Craq9z0t.mjs → stim-installations-CINXy_su.mjs} +3 -122
- package/dist/{stop-C8JW6kKp.mjs → stop-Dj4pfmyv.mjs} +111 -31
- package/dist/{stop-B1kOpYei.mjs → stop-ZvOeTmKb.mjs} +2 -2
- package/dist/supervisor-run.d.mts +11 -2
- package/dist/supervisor-run.mjs +228 -42
- package/dist/{workspace-process-lock-BYfZz_S-.mjs → workspace-process-lock-BXA96myD.mjs} +14 -9
- package/dist/workspaces-BTUyFfkv.mjs +1003 -0
- package/dist/worktree-4aZg2NnI.mjs +2 -0
- package/dist/{worktree-CFLA-6MX.mjs → worktree-BMS7GeqB.mjs} +523 -61
- package/dist/{worktree-FG_ZKKdb.mjs → worktree-BlizKDpZ.mjs} +200 -54
- package/dist/{xcode-B6YaWeg0.mjs → xcode-B2tBkf0m.mjs} +66 -57
- package/package.json +6 -6
- package/skill/SKILL.md +2 -0
- package/dist/app-install-BDbsSgCl.mjs +0 -1175
- package/dist/dev-client-BmT-ioFI.mjs +0 -797
- package/dist/device-lease-Ffz_M5t0.mjs +0 -359
- package/dist/exec-B29XNuhP.mjs +0 -77
- package/dist/logs-query-BGL-Pl12.mjs +0 -290
- package/dist/metro-C7bcJ0uo.mjs +0 -233
- package/dist/ownership-wolE-0Do.mjs +0 -470
- package/dist/public-http-probe-CyMDrQ3c.mjs +0 -61
- package/dist/reclaim-KJ-0uWEF.mjs +0 -361
- package/dist/server-expo-YDDG3eP7.mjs +0 -2
- package/dist/spawn-entry-nCE6m770.mjs +0 -14
- package/dist/status-DdkLki47.mjs +0 -426
- package/dist/workspace-state-BT5E3SA2.mjs +0 -146
- package/dist/{metro-store-CDDZHtPl.mjs → metro-store-DzeWkfqW.mjs} +1 -1
|
@@ -1,1175 +0,0 @@
|
|
|
1
|
-
import { t as getExecutor } from "./exec-B29XNuhP.mjs";
|
|
2
|
-
import { o as parseNdjsonText } from "./ndjson-BZwI1NBv.mjs";
|
|
3
|
-
import { t as DEV_MENU_LAUNCH_ARGS } from "./ios-device-CFXA46ez.mjs";
|
|
4
|
-
import { s as isAppLaunchError } from "./command-output-B3ycpsYv.mjs";
|
|
5
|
-
import { l as iosSimulatorFailureAdvice, p as listUserApps, w as uninstallIosApp } from "./ios-BcCiI4TS.mjs";
|
|
6
|
-
import { closeSync, lstatSync, openSync, readFileSync, readSync, readdirSync } from "node:fs";
|
|
7
|
-
import { createHash } from "node:crypto";
|
|
8
|
-
import { isAbsolute, join } from "node:path";
|
|
9
|
-
//#region src/engine/installed-artifact.ts
|
|
10
|
-
const READ_CHUNK_BYTES = 1048576;
|
|
11
|
-
const DEVICE_PATH_TIMEOUT_MS = 3e4;
|
|
12
|
-
const DEVICE_HASH_TIMEOUT_MS = 12e4;
|
|
13
|
-
const SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
14
|
-
function parseInstalledApkPath(text) {
|
|
15
|
-
if (typeof text !== "string") return null;
|
|
16
|
-
const paths = [];
|
|
17
|
-
for (const raw of text.split("\n")) {
|
|
18
|
-
const line = raw.trim();
|
|
19
|
-
if (!line.startsWith("package:")) continue;
|
|
20
|
-
const path = line.slice(8);
|
|
21
|
-
if (path.startsWith("/")) paths.push(path);
|
|
22
|
-
}
|
|
23
|
-
return paths.length === 1 ? paths[0] : null;
|
|
24
|
-
}
|
|
25
|
-
function parseDeviceSha256(text) {
|
|
26
|
-
if (typeof text !== "string") return null;
|
|
27
|
-
const first = text.trim().split(/\s+/)[0] ?? "";
|
|
28
|
-
return SHA256_HEX.test(first) ? first : null;
|
|
29
|
-
}
|
|
30
|
-
function parseAppContainerPath(text) {
|
|
31
|
-
if (typeof text !== "string") return null;
|
|
32
|
-
const lines = text.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
33
|
-
return lines.length === 1 && isAbsolute(lines[0]) ? lines[0] : null;
|
|
34
|
-
}
|
|
35
|
-
function artifactsMatch(local, installed) {
|
|
36
|
-
return Boolean(local) && local === installed;
|
|
37
|
-
}
|
|
38
|
-
function hashFile(file) {
|
|
39
|
-
let fd = null;
|
|
40
|
-
try {
|
|
41
|
-
fd = openSync(file, "r");
|
|
42
|
-
const digest = createHash("sha256");
|
|
43
|
-
const buffer = Buffer.allocUnsafe(READ_CHUNK_BYTES);
|
|
44
|
-
for (;;) {
|
|
45
|
-
const read = readSync(fd, buffer, 0, READ_CHUNK_BYTES, null);
|
|
46
|
-
if (read <= 0) break;
|
|
47
|
-
digest.update(buffer.subarray(0, read));
|
|
48
|
-
}
|
|
49
|
-
return digest.digest("hex");
|
|
50
|
-
} catch {
|
|
51
|
-
return null;
|
|
52
|
-
} finally {
|
|
53
|
-
if (fd !== null) try {
|
|
54
|
-
closeSync(fd);
|
|
55
|
-
} catch {}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
function hashBundle(dir) {
|
|
59
|
-
const digest = createHash("sha256");
|
|
60
|
-
const walk = (abs, rel) => {
|
|
61
|
-
let names;
|
|
62
|
-
try {
|
|
63
|
-
names = readdirSync(abs);
|
|
64
|
-
} catch {
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
for (const name of names.toSorted()) {
|
|
68
|
-
const child = join(abs, name);
|
|
69
|
-
const path = rel === "" ? name : `${rel}/${name}`;
|
|
70
|
-
let stat;
|
|
71
|
-
try {
|
|
72
|
-
stat = lstatSync(child);
|
|
73
|
-
} catch {
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
if (stat.isDirectory()) {
|
|
77
|
-
if (!walk(child, path)) return false;
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
if (!stat.isFile()) return false;
|
|
81
|
-
const file = hashFile(child);
|
|
82
|
-
if (file === null) return false;
|
|
83
|
-
digest.update(`${path}\0${file}\n`);
|
|
84
|
-
}
|
|
85
|
-
return true;
|
|
86
|
-
};
|
|
87
|
-
return walk(dir, "") ? digest.digest("hex") : null;
|
|
88
|
-
}
|
|
89
|
-
function deviceHoldsApk({ serial, packageName, apkPath }, { exec = null } = {}) {
|
|
90
|
-
const e = exec || getExecutor();
|
|
91
|
-
let path;
|
|
92
|
-
try {
|
|
93
|
-
path = parseInstalledApkPath(e.runFile("adb", [
|
|
94
|
-
"-s",
|
|
95
|
-
serial,
|
|
96
|
-
"shell",
|
|
97
|
-
"pm",
|
|
98
|
-
"path",
|
|
99
|
-
packageName
|
|
100
|
-
], { timeoutMs: DEVICE_PATH_TIMEOUT_MS }));
|
|
101
|
-
} catch {
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
if (!path) return false;
|
|
105
|
-
let installed;
|
|
106
|
-
try {
|
|
107
|
-
installed = parseDeviceSha256(e.runFile("adb", [
|
|
108
|
-
"-s",
|
|
109
|
-
serial,
|
|
110
|
-
"shell",
|
|
111
|
-
"sha256sum",
|
|
112
|
-
path
|
|
113
|
-
], { timeoutMs: DEVICE_HASH_TIMEOUT_MS }));
|
|
114
|
-
} catch {
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
if (!installed) return false;
|
|
118
|
-
return artifactsMatch(hashFile(apkPath), installed);
|
|
119
|
-
}
|
|
120
|
-
function deviceHoldsBundle({ udid, bundleId, appPath }, { exec = null } = {}) {
|
|
121
|
-
const e = exec || getExecutor();
|
|
122
|
-
let container;
|
|
123
|
-
try {
|
|
124
|
-
container = parseAppContainerPath(e.runFile("xcrun", [
|
|
125
|
-
"simctl",
|
|
126
|
-
"get_app_container",
|
|
127
|
-
udid,
|
|
128
|
-
bundleId
|
|
129
|
-
], { timeoutMs: DEVICE_PATH_TIMEOUT_MS }));
|
|
130
|
-
} catch {
|
|
131
|
-
return false;
|
|
132
|
-
}
|
|
133
|
-
if (!container) return false;
|
|
134
|
-
const installed = hashBundle(container);
|
|
135
|
-
if (!installed) return false;
|
|
136
|
-
return artifactsMatch(hashBundle(appPath), installed);
|
|
137
|
-
}
|
|
138
|
-
//#endregion
|
|
139
|
-
//#region src/engine/app-readiness.ts
|
|
140
|
-
const APP_READINESS_TIMEOUT_MS = 3e4;
|
|
141
|
-
function appReadinessSignal(record, platform) {
|
|
142
|
-
if (record.src !== "device" || !platform || record.platform !== platform) return null;
|
|
143
|
-
if (record.level !== "info" && record.level !== "debug") return null;
|
|
144
|
-
if (typeof record.msg !== "string") return null;
|
|
145
|
-
const text = record.msg.trim();
|
|
146
|
-
const message = /^(?:'[^']*'|"[^"]*")$/.test(text) ? text.slice(1, -1) : text;
|
|
147
|
-
return /^\[stim:readiness\] (pending|ready)$/.exec(message)?.[1] ?? null;
|
|
148
|
-
}
|
|
149
|
-
//#endregion
|
|
150
|
-
//#region src/engine/app-install.ts
|
|
151
|
-
const INSTALL_ERROR = "STIM_INSTALL_FAILED";
|
|
152
|
-
const LAUNCH_ERROR = "STIM_LAUNCH_FAILED";
|
|
153
|
-
const DEFAULT_METRO_PORT = 8081;
|
|
154
|
-
const IOS_SCHEME_APPROVAL_DOMAIN = "com.apple.launchservices.schemeapproval";
|
|
155
|
-
const IOS_SCHEME_APPROVAL_OPENER = "com.apple.CoreSimulator.CoreSimulatorBridge";
|
|
156
|
-
const IOS_DEV_MENU_OFF_KEYS = ["EXDevMenuShowsAtLaunch", "EXDevMenuShowFloatingActionButton"];
|
|
157
|
-
const DEV_CLIENT_ONBOARDING_QUERY = "disableOnboarding=1";
|
|
158
|
-
const DEV_CLIENT_DISABLE_FAB_QUERY = "disableFab=1";
|
|
159
|
-
const ANDROID_DISABLE_AUTO_LAUNCH_EXTRA = "EXDevMenuDisableAutoLaunch";
|
|
160
|
-
function describeIosSimulatorFailure(error, exec) {
|
|
161
|
-
const detail = describe(error);
|
|
162
|
-
return error?.code === "ETIMEDOUT" ? `${detail}. ${iosSimulatorFailureAdvice(exec)}` : detail;
|
|
163
|
-
}
|
|
164
|
-
function installIosApp({ udid, appPath, bundleId = null, devClientScheme = null, proveInstalled = true }, { exec = null, now = null } = {}) {
|
|
165
|
-
const e = exec || getExecutor();
|
|
166
|
-
const artifactStartedAt = now?.();
|
|
167
|
-
const skipped = bundleId && proveInstalled ? deviceHoldsBundle({
|
|
168
|
-
udid,
|
|
169
|
-
bundleId,
|
|
170
|
-
appPath
|
|
171
|
-
}, { exec: e }) : false;
|
|
172
|
-
if (!skipped) try {
|
|
173
|
-
e.runFile("xcrun", [
|
|
174
|
-
"simctl",
|
|
175
|
-
"install",
|
|
176
|
-
udid,
|
|
177
|
-
appPath
|
|
178
|
-
], {
|
|
179
|
-
timeoutMs: 3e5,
|
|
180
|
-
killSignal: "SIGKILL"
|
|
181
|
-
});
|
|
182
|
-
} catch (err) {
|
|
183
|
-
return {
|
|
184
|
-
failed: true,
|
|
185
|
-
code: INSTALL_ERROR,
|
|
186
|
-
reason: `simctl install failed for ${appPath}: ${describeIosSimulatorFailure(err, e)}`
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
const artifactFinishedAt = now?.();
|
|
190
|
-
const artifactDurationMs = artifactStartedAt !== void 0 && artifactFinishedAt !== void 0 ? artifactFinishedAt - artifactStartedAt : void 0;
|
|
191
|
-
const preparationStartedAt = bundleId && devClientScheme ? artifactFinishedAt : void 0;
|
|
192
|
-
if (bundleId && devClientScheme) try {
|
|
193
|
-
for (const key of IOS_DEV_MENU_OFF_KEYS) e.runFile("xcrun", [
|
|
194
|
-
"simctl",
|
|
195
|
-
"spawn",
|
|
196
|
-
udid,
|
|
197
|
-
"defaults",
|
|
198
|
-
"write",
|
|
199
|
-
bundleId,
|
|
200
|
-
key,
|
|
201
|
-
"-bool",
|
|
202
|
-
"false"
|
|
203
|
-
], { timeoutMs: 6e4 });
|
|
204
|
-
for (const key of iosSchemeApprovalKeys(bundleId, devClientScheme)) e.runFile("xcrun", [
|
|
205
|
-
"simctl",
|
|
206
|
-
"spawn",
|
|
207
|
-
udid,
|
|
208
|
-
"defaults",
|
|
209
|
-
"write",
|
|
210
|
-
IOS_SCHEME_APPROVAL_DOMAIN,
|
|
211
|
-
key,
|
|
212
|
-
"-string",
|
|
213
|
-
bundleId
|
|
214
|
-
], { timeoutMs: 6e4 });
|
|
215
|
-
} catch (err) {
|
|
216
|
-
return {
|
|
217
|
-
failed: true,
|
|
218
|
-
code: INSTALL_ERROR,
|
|
219
|
-
reason: `Installed ${bundleId}, but could not prepare the dev client: ${describeIosSimulatorFailure(err, e)}`
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
const devClientPreparationDurationMs = now && preparationStartedAt !== void 0 ? now() - preparationStartedAt : void 0;
|
|
223
|
-
const timing = {
|
|
224
|
-
...artifactDurationMs === void 0 ? {} : { artifactDurationMs },
|
|
225
|
-
...devClientPreparationDurationMs === void 0 ? {} : { devClientPreparationDurationMs }
|
|
226
|
-
};
|
|
227
|
-
return skipped ? {
|
|
228
|
-
ok: true,
|
|
229
|
-
appPath,
|
|
230
|
-
skipped: true,
|
|
231
|
-
...timing
|
|
232
|
-
} : {
|
|
233
|
-
ok: true,
|
|
234
|
-
appPath,
|
|
235
|
-
...timing
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
function clearOtherUserApps({ udid, keep }, { list = listUserApps, uninstall = uninstallIosApp } = {}) {
|
|
239
|
-
const removed = [];
|
|
240
|
-
const failed = [];
|
|
241
|
-
let installed;
|
|
242
|
-
try {
|
|
243
|
-
installed = list(udid);
|
|
244
|
-
} catch {
|
|
245
|
-
return {
|
|
246
|
-
listed: false,
|
|
247
|
-
removed,
|
|
248
|
-
failed
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
for (const bundleId of installed) {
|
|
252
|
-
if (bundleId === keep) continue;
|
|
253
|
-
try {
|
|
254
|
-
uninstall(udid, bundleId);
|
|
255
|
-
removed.push(bundleId);
|
|
256
|
-
} catch {
|
|
257
|
-
failed.push(bundleId);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
return {
|
|
261
|
-
listed: true,
|
|
262
|
-
removed,
|
|
263
|
-
failed
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
function jsLocationValue(metroPort) {
|
|
267
|
-
return `localhost:${metroPort}`;
|
|
268
|
-
}
|
|
269
|
-
function devClientDeepLink(scheme, projectOrigin) {
|
|
270
|
-
const projectUrl = `${projectOrigin.replace(/\/+$/, "")}/?${DEV_CLIENT_ONBOARDING_QUERY}`;
|
|
271
|
-
return `${scheme}://expo-development-client/?url=${encodeURIComponent(projectUrl)}&${DEV_CLIENT_DISABLE_FAB_QUERY}`;
|
|
272
|
-
}
|
|
273
|
-
function devClientUrl(scheme, metroPort, host = "localhost") {
|
|
274
|
-
return devClientDeepLink(scheme, `http://${host}:${metroPort}`);
|
|
275
|
-
}
|
|
276
|
-
function iosSchemeApprovalKeys(bundleId, devClientScheme) {
|
|
277
|
-
return [.../* @__PURE__ */ new Set([bundleId, devClientScheme])].map((target) => `${IOS_SCHEME_APPROVAL_OPENER}-->${target}`);
|
|
278
|
-
}
|
|
279
|
-
function parseLaunchedPid(text) {
|
|
280
|
-
if (typeof text !== "string") return null;
|
|
281
|
-
const match = text.trim().match(/:\s*(\d+)\s*$/);
|
|
282
|
-
if (!match) return null;
|
|
283
|
-
const pid = Number(match[1]);
|
|
284
|
-
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
285
|
-
}
|
|
286
|
-
function iosAppProcess(udid, bundleId, { exec = null } = {}) {
|
|
287
|
-
const e = exec || getExecutor();
|
|
288
|
-
let out = "";
|
|
289
|
-
try {
|
|
290
|
-
out = e.runFile("xcrun", [
|
|
291
|
-
"simctl",
|
|
292
|
-
"spawn",
|
|
293
|
-
udid,
|
|
294
|
-
"launchctl",
|
|
295
|
-
"list"
|
|
296
|
-
], { timeoutMs: 2e3 });
|
|
297
|
-
} catch {
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
const label = `UIKitApplication:${bundleId}[`;
|
|
301
|
-
for (const line of out.split("\n")) {
|
|
302
|
-
if (!line.includes(label)) continue;
|
|
303
|
-
const pid = Number(line.trim().split(/\s+/)[0]);
|
|
304
|
-
if (Number.isInteger(pid) && pid > 0) return pid;
|
|
305
|
-
}
|
|
306
|
-
return null;
|
|
307
|
-
}
|
|
308
|
-
function launchedIosAppAfterNoHandle(error, udid, bundleId, exec) {
|
|
309
|
-
const stderr = String(error?.stderr ?? "");
|
|
310
|
-
if (!stderr.includes("domain=NSPOSIXErrorDomain, code=3") || !stderr.includes(`Application launch for '${bundleId}' did not return a process handle nor launch error.`)) return null;
|
|
311
|
-
return iosAppProcess(udid, bundleId, { exec }) ?? null;
|
|
312
|
-
}
|
|
313
|
-
function launchIosApp({ udid, bundleId, metroPort, devClientScheme = null, consolePaths }, { exec = null } = {}) {
|
|
314
|
-
const e = exec || getExecutor();
|
|
315
|
-
const launchArgs = [
|
|
316
|
-
"simctl",
|
|
317
|
-
"launch",
|
|
318
|
-
...consolePaths ? [`--stdout=${consolePaths.stdout}`, `--stderr=${consolePaths.stderr}`] : [],
|
|
319
|
-
udid,
|
|
320
|
-
bundleId
|
|
321
|
-
];
|
|
322
|
-
if (metroPort !== null) {
|
|
323
|
-
try {
|
|
324
|
-
e.runFile("xcrun", [
|
|
325
|
-
"simctl",
|
|
326
|
-
"spawn",
|
|
327
|
-
udid,
|
|
328
|
-
"defaults",
|
|
329
|
-
"write",
|
|
330
|
-
bundleId,
|
|
331
|
-
"RCT_jsLocation",
|
|
332
|
-
jsLocationValue(metroPort)
|
|
333
|
-
], { timeoutMs: 6e4 });
|
|
334
|
-
} catch (err) {
|
|
335
|
-
return {
|
|
336
|
-
failed: true,
|
|
337
|
-
code: LAUNCH_ERROR,
|
|
338
|
-
reason: `Could not point ${bundleId} at Metro port ${metroPort} (defaults write RCT_jsLocation): ${describeIosSimulatorFailure(err, e)}`
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
if (devClientScheme) {
|
|
342
|
-
const url = devClientUrl(devClientScheme, metroPort);
|
|
343
|
-
let launchedWithInitialUrl = false;
|
|
344
|
-
try {
|
|
345
|
-
if (consolePaths && iosAppProcess(udid, bundleId, { exec: e }) === null) {
|
|
346
|
-
const initialUrl = new URL(url).searchParams.get("url");
|
|
347
|
-
launchedWithInitialUrl = true;
|
|
348
|
-
const pid = parseLaunchedPid(e.runFile("xcrun", [
|
|
349
|
-
...launchArgs,
|
|
350
|
-
"--initialUrl",
|
|
351
|
-
initialUrl
|
|
352
|
-
], { timeoutMs: 6e4 }));
|
|
353
|
-
return {
|
|
354
|
-
ok: true,
|
|
355
|
-
mode: "launch",
|
|
356
|
-
url,
|
|
357
|
-
jsLocation: jsLocationValue(metroPort),
|
|
358
|
-
pid
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
e.runFile("xcrun", [
|
|
362
|
-
"simctl",
|
|
363
|
-
"openurl",
|
|
364
|
-
udid,
|
|
365
|
-
url
|
|
366
|
-
], { timeoutMs: 6e4 });
|
|
367
|
-
return {
|
|
368
|
-
ok: true,
|
|
369
|
-
mode: "openurl",
|
|
370
|
-
url,
|
|
371
|
-
jsLocation: jsLocationValue(metroPort)
|
|
372
|
-
};
|
|
373
|
-
} catch (err) {
|
|
374
|
-
const pid = launchedWithInitialUrl ? launchedIosAppAfterNoHandle(err, udid, bundleId, e) : null;
|
|
375
|
-
if (pid) return {
|
|
376
|
-
ok: true,
|
|
377
|
-
mode: "launch",
|
|
378
|
-
url,
|
|
379
|
-
jsLocation: jsLocationValue(metroPort),
|
|
380
|
-
pid
|
|
381
|
-
};
|
|
382
|
-
return {
|
|
383
|
-
failed: true,
|
|
384
|
-
code: LAUNCH_ERROR,
|
|
385
|
-
reason: `simctl ${launchedWithInitialUrl ? `launch ${bundleId}` : `openurl ${url}`} failed: ${describeIosSimulatorFailure(err, e)}`
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
const preLaunchPid = iosAppProcess(udid, bundleId, { exec: e });
|
|
391
|
-
try {
|
|
392
|
-
const result = {
|
|
393
|
-
ok: true,
|
|
394
|
-
mode: "launch",
|
|
395
|
-
pid: parseLaunchedPid(e.runFile("xcrun", launchArgs, { timeoutMs: 6e4 }))
|
|
396
|
-
};
|
|
397
|
-
if (metroPort !== null) result.jsLocation = jsLocationValue(metroPort);
|
|
398
|
-
return result;
|
|
399
|
-
} catch (err) {
|
|
400
|
-
const pid = preLaunchPid === null ? launchedIosAppAfterNoHandle(err, udid, bundleId, e) : null;
|
|
401
|
-
if (pid) {
|
|
402
|
-
const result = {
|
|
403
|
-
ok: true,
|
|
404
|
-
mode: "launch",
|
|
405
|
-
pid
|
|
406
|
-
};
|
|
407
|
-
if (metroPort !== null) result.jsLocation = jsLocationValue(metroPort);
|
|
408
|
-
return result;
|
|
409
|
-
}
|
|
410
|
-
return {
|
|
411
|
-
failed: true,
|
|
412
|
-
code: LAUNCH_ERROR,
|
|
413
|
-
reason: `simctl launch ${bundleId} failed: ${describeIosSimulatorFailure(err, e)}`
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
function installConflictKind(text) {
|
|
418
|
-
const out = String(text ?? "");
|
|
419
|
-
if (/INSTALL_FAILED_UPDATE_INCOMPATIBLE|INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES|signatures do not match/i.test(out)) return "signature";
|
|
420
|
-
if (/INSTALL_FAILED_VERSION_DOWNGRADE/i.test(out)) return "downgrade";
|
|
421
|
-
return null;
|
|
422
|
-
}
|
|
423
|
-
const ADB_INSTALL_TIMEOUT_MS = 3e5;
|
|
424
|
-
function installAndroidApp({ serial, apkPath, packageName = null, allowUninstall = false }, { exec = null } = {}) {
|
|
425
|
-
const e = exec || getExecutor();
|
|
426
|
-
if (packageName && deviceHoldsApk({
|
|
427
|
-
serial,
|
|
428
|
-
packageName,
|
|
429
|
-
apkPath
|
|
430
|
-
}, { exec: e })) return {
|
|
431
|
-
ok: true,
|
|
432
|
-
apkPath,
|
|
433
|
-
skipped: true
|
|
434
|
-
};
|
|
435
|
-
const install = () => {
|
|
436
|
-
e.runFile("adb", [
|
|
437
|
-
"-s",
|
|
438
|
-
serial,
|
|
439
|
-
"install",
|
|
440
|
-
"-r",
|
|
441
|
-
apkPath
|
|
442
|
-
], { timeoutMs: ADB_INSTALL_TIMEOUT_MS });
|
|
443
|
-
};
|
|
444
|
-
try {
|
|
445
|
-
install();
|
|
446
|
-
return {
|
|
447
|
-
ok: true,
|
|
448
|
-
apkPath
|
|
449
|
-
};
|
|
450
|
-
} catch (err) {
|
|
451
|
-
const conflict = installConflictKind(describe(err));
|
|
452
|
-
if (!conflict || !allowUninstall || !packageName) return {
|
|
453
|
-
failed: true,
|
|
454
|
-
code: INSTALL_ERROR,
|
|
455
|
-
reason: `adb install failed for ${apkPath}: ${describe(err)}`
|
|
456
|
-
};
|
|
457
|
-
try {
|
|
458
|
-
e.runFile("adb", [
|
|
459
|
-
"-s",
|
|
460
|
-
serial,
|
|
461
|
-
"uninstall",
|
|
462
|
-
packageName
|
|
463
|
-
]);
|
|
464
|
-
} catch (uninstallErr) {
|
|
465
|
-
return {
|
|
466
|
-
failed: true,
|
|
467
|
-
code: INSTALL_ERROR,
|
|
468
|
-
reason: `adb install failed for ${apkPath} (${conflict}) and ${packageName} could not be uninstalled: ` + describe(uninstallErr)
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
try {
|
|
472
|
-
install();
|
|
473
|
-
} catch (retryErr) {
|
|
474
|
-
return {
|
|
475
|
-
failed: true,
|
|
476
|
-
code: INSTALL_ERROR,
|
|
477
|
-
reason: `adb install failed for ${apkPath} even after uninstalling ${packageName}: ${describe(retryErr)}`
|
|
478
|
-
};
|
|
479
|
-
}
|
|
480
|
-
return {
|
|
481
|
-
ok: true,
|
|
482
|
-
apkPath,
|
|
483
|
-
uninstalled: true,
|
|
484
|
-
note: conflict === "signature" ? `${packageName} was already installed with a different signer, so it was uninstalled (its data went with it) before this APK could be installed` : `${packageName} was already installed at a higher versionCode, so it was uninstalled (its data went with it) before this APK could be installed`
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
function parseResolvedActivity(text) {
|
|
489
|
-
if (typeof text !== "string") return null;
|
|
490
|
-
for (const raw of text.split("\n")) {
|
|
491
|
-
const line = raw.trim();
|
|
492
|
-
if (!line) continue;
|
|
493
|
-
if (/^No activity found/i.test(line)) return null;
|
|
494
|
-
if (line.includes("=")) continue;
|
|
495
|
-
if (!line.includes("/")) continue;
|
|
496
|
-
return line;
|
|
497
|
-
}
|
|
498
|
-
return null;
|
|
499
|
-
}
|
|
500
|
-
function resolveLaunchActivity(serial, packageName, { exec = null } = {}) {
|
|
501
|
-
const e = exec || getExecutor();
|
|
502
|
-
try {
|
|
503
|
-
return parseResolvedActivity(e.runFile("adb", [
|
|
504
|
-
"-s",
|
|
505
|
-
serial,
|
|
506
|
-
"shell",
|
|
507
|
-
"cmd",
|
|
508
|
-
"package",
|
|
509
|
-
"resolve-activity",
|
|
510
|
-
"--brief",
|
|
511
|
-
"-c",
|
|
512
|
-
"android.intent.category.LAUNCHER",
|
|
513
|
-
packageName
|
|
514
|
-
]));
|
|
515
|
-
} catch {
|
|
516
|
-
return null;
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
function reverseMetroPorts({ serial, metroPort, devicePorts = null }, { exec = null } = {}) {
|
|
520
|
-
const e = exec || getExecutor();
|
|
521
|
-
const pairs = (devicePorts ?? [metroPort]).map((device) => [device, metroPort]);
|
|
522
|
-
for (const [device, host] of pairs) try {
|
|
523
|
-
e.runFile("adb", [
|
|
524
|
-
"-s",
|
|
525
|
-
serial,
|
|
526
|
-
"reverse",
|
|
527
|
-
`tcp:${device}`,
|
|
528
|
-
`tcp:${host}`
|
|
529
|
-
]);
|
|
530
|
-
} catch (err) {
|
|
531
|
-
return {
|
|
532
|
-
failed: true,
|
|
533
|
-
code: LAUNCH_ERROR,
|
|
534
|
-
reason: `adb reverse tcp:${device} tcp:${host} failed on ${serial}: ${describe(err)}`
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
return {
|
|
538
|
-
ok: true,
|
|
539
|
-
reversed: pairs.map(([device, host]) => `tcp:${device}->tcp:${host}`)
|
|
540
|
-
};
|
|
541
|
-
}
|
|
542
|
-
const EMULATOR_HOST_LOOPBACK = "127.0.0.1";
|
|
543
|
-
const PHYSICAL_HOST_LOOPBACK = "localhost";
|
|
544
|
-
function androidMetroHost(physical) {
|
|
545
|
-
return physical ? PHYSICAL_HOST_LOOPBACK : EMULATOR_HOST_LOOPBACK;
|
|
546
|
-
}
|
|
547
|
-
function deviceShellArg(text) {
|
|
548
|
-
return `'${String(text).replace(/'/g, "'\\''")}'`;
|
|
549
|
-
}
|
|
550
|
-
function debugHttpHostScript({ packageName, host, dataDir = null }) {
|
|
551
|
-
const dir = dataDir || `/data/data/${packageName}`;
|
|
552
|
-
const prefs = `shared_prefs/${packageName}_preferences.xml`;
|
|
553
|
-
const tmp = `${prefs}.stim.tmp`;
|
|
554
|
-
return [
|
|
555
|
-
`cd ${dir} || exit 1`,
|
|
556
|
-
"mkdir -p shared_prefs || exit 1",
|
|
557
|
-
`printf '%s\\n' '<?xml version="1.0" encoding="utf-8" standalone="yes" ?>' '<map>' > ${tmp} || exit 1`,
|
|
558
|
-
`if [ -f ${prefs} ]; then grep -v 'debug_http_host' ${prefs} | grep -v '<?xml' | grep -v '<map' | grep -v '</map>' >> ${tmp}; fi`,
|
|
559
|
-
`printf '%s\\n' ' <string name="debug_http_host">${host}</string>' '</map>' >> ${tmp} || exit 1`,
|
|
560
|
-
`mv ${tmp} ${prefs} || exit 1`,
|
|
561
|
-
`grep -q '>${host}<' ${prefs} || exit 1`
|
|
562
|
-
].join("\n");
|
|
563
|
-
}
|
|
564
|
-
function writeDebugHttpHost({ serial, packageName, metroPort, physical = false }, { exec = null } = {}) {
|
|
565
|
-
const e = exec || getExecutor();
|
|
566
|
-
const host = `${androidMetroHost(physical)}:${metroPort}`;
|
|
567
|
-
const script = debugHttpHostScript({
|
|
568
|
-
packageName,
|
|
569
|
-
host
|
|
570
|
-
});
|
|
571
|
-
try {
|
|
572
|
-
e.runFile("adb", [
|
|
573
|
-
"-s",
|
|
574
|
-
serial,
|
|
575
|
-
"shell",
|
|
576
|
-
"run-as",
|
|
577
|
-
packageName,
|
|
578
|
-
"sh",
|
|
579
|
-
"-c",
|
|
580
|
-
deviceShellArg(script)
|
|
581
|
-
]);
|
|
582
|
-
return {
|
|
583
|
-
ok: true,
|
|
584
|
-
host
|
|
585
|
-
};
|
|
586
|
-
} catch (err) {
|
|
587
|
-
return {
|
|
588
|
-
ok: false,
|
|
589
|
-
reason: `debug_http_host not written (${describe(err)}); relying on adb reverse`
|
|
590
|
-
};
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
function androidDevClientUrl(scheme, metroPort, physical = false) {
|
|
594
|
-
return devClientUrl(scheme, metroPort, androidMetroHost(physical));
|
|
595
|
-
}
|
|
596
|
-
function amStartError(text) {
|
|
597
|
-
const out = String(text ?? "");
|
|
598
|
-
for (const raw of out.split("\n")) {
|
|
599
|
-
const line = raw.trim();
|
|
600
|
-
if (/^Error:/i.test(line)) return line;
|
|
601
|
-
}
|
|
602
|
-
return null;
|
|
603
|
-
}
|
|
604
|
-
function openAndroidDevClientUrl({ serial, url, packageName }, { exec = null } = {}) {
|
|
605
|
-
const e = exec || getExecutor();
|
|
606
|
-
let out;
|
|
607
|
-
try {
|
|
608
|
-
const args = [
|
|
609
|
-
"-s",
|
|
610
|
-
serial,
|
|
611
|
-
"shell",
|
|
612
|
-
"am",
|
|
613
|
-
"start",
|
|
614
|
-
"-a",
|
|
615
|
-
"android.intent.action.VIEW",
|
|
616
|
-
"-d",
|
|
617
|
-
deviceShellArg(url),
|
|
618
|
-
...packageName ? ["-p", packageName] : [],
|
|
619
|
-
"--ez",
|
|
620
|
-
ANDROID_DISABLE_AUTO_LAUNCH_EXTRA,
|
|
621
|
-
"true"
|
|
622
|
-
];
|
|
623
|
-
out = e.runFile("adb", args);
|
|
624
|
-
} catch (err) {
|
|
625
|
-
return {
|
|
626
|
-
failed: true,
|
|
627
|
-
reason: `am start -d ${url} failed on ${serial}: ${describe(err)}`
|
|
628
|
-
};
|
|
629
|
-
}
|
|
630
|
-
const error = amStartError(out);
|
|
631
|
-
if (error) return {
|
|
632
|
-
failed: true,
|
|
633
|
-
reason: `am start -d ${url} did not start anything on ${serial}: ${error}`
|
|
634
|
-
};
|
|
635
|
-
return {
|
|
636
|
-
ok: true,
|
|
637
|
-
url
|
|
638
|
-
};
|
|
639
|
-
}
|
|
640
|
-
function launchAndroidApp({ serial, packageName, metroPort, devClientScheme = null, physical = false }, { exec = null } = {}) {
|
|
641
|
-
const e = exec || getExecutor();
|
|
642
|
-
const reversed = reverseMetroPorts({
|
|
643
|
-
serial,
|
|
644
|
-
metroPort
|
|
645
|
-
}, { exec: e });
|
|
646
|
-
if (reversed.failed) return reversed;
|
|
647
|
-
const prefs = writeDebugHttpHost({
|
|
648
|
-
serial,
|
|
649
|
-
packageName,
|
|
650
|
-
metroPort,
|
|
651
|
-
physical
|
|
652
|
-
}, { exec: e });
|
|
653
|
-
let reversedPairs = reversed.reversed ?? [];
|
|
654
|
-
if (!prefs.ok && Number(metroPort) !== 8081) {
|
|
655
|
-
const fallback = reverseMetroPorts({
|
|
656
|
-
serial,
|
|
657
|
-
metroPort,
|
|
658
|
-
devicePorts: [DEFAULT_METRO_PORT]
|
|
659
|
-
}, { exec: e });
|
|
660
|
-
if (fallback.failed) return fallback;
|
|
661
|
-
reversedPairs = [...reversedPairs, ...fallback.reversed ?? []];
|
|
662
|
-
}
|
|
663
|
-
const wiring = {
|
|
664
|
-
reversed: reversedPairs,
|
|
665
|
-
debugHttpHost: prefs.ok ? prefs.host : null,
|
|
666
|
-
debugHttpHostNote: prefs.ok ? null : prefs.reason
|
|
667
|
-
};
|
|
668
|
-
let devClientNote = null;
|
|
669
|
-
if (devClientScheme) {
|
|
670
|
-
const url = androidDevClientUrl(devClientScheme, metroPort, physical);
|
|
671
|
-
const opened = openAndroidDevClientUrl({
|
|
672
|
-
serial,
|
|
673
|
-
url
|
|
674
|
-
}, { exec: e });
|
|
675
|
-
if (opened.ok) return {
|
|
676
|
-
ok: true,
|
|
677
|
-
mode: "deep-link",
|
|
678
|
-
devClientUrl: url,
|
|
679
|
-
...wiring
|
|
680
|
-
};
|
|
681
|
-
devClientNote = `${opened.reason}; fell back to the launcher activity`;
|
|
682
|
-
}
|
|
683
|
-
const component = resolveLaunchActivity(serial, packageName, { exec: e });
|
|
684
|
-
if (component) try {
|
|
685
|
-
e.runFile("adb", [
|
|
686
|
-
"-s",
|
|
687
|
-
serial,
|
|
688
|
-
"shell",
|
|
689
|
-
"am",
|
|
690
|
-
"start",
|
|
691
|
-
"-n",
|
|
692
|
-
component
|
|
693
|
-
]);
|
|
694
|
-
return {
|
|
695
|
-
ok: true,
|
|
696
|
-
mode: "am-start",
|
|
697
|
-
component,
|
|
698
|
-
devClientNote,
|
|
699
|
-
...wiring
|
|
700
|
-
};
|
|
701
|
-
} catch (err) {
|
|
702
|
-
return {
|
|
703
|
-
failed: true,
|
|
704
|
-
code: LAUNCH_ERROR,
|
|
705
|
-
reason: `am start -n ${component} failed on ${serial}: ${describe(err)}`
|
|
706
|
-
};
|
|
707
|
-
}
|
|
708
|
-
try {
|
|
709
|
-
e.runFile("adb", [
|
|
710
|
-
"-s",
|
|
711
|
-
serial,
|
|
712
|
-
"shell",
|
|
713
|
-
"monkey",
|
|
714
|
-
"-p",
|
|
715
|
-
packageName,
|
|
716
|
-
"1"
|
|
717
|
-
]);
|
|
718
|
-
return {
|
|
719
|
-
ok: true,
|
|
720
|
-
mode: "monkey",
|
|
721
|
-
devClientNote,
|
|
722
|
-
...wiring
|
|
723
|
-
};
|
|
724
|
-
} catch (err) {
|
|
725
|
-
return {
|
|
726
|
-
failed: true,
|
|
727
|
-
code: LAUNCH_ERROR,
|
|
728
|
-
reason: `Could not launch ${packageName} on ${serial}: no launcher activity resolved and monkey failed: ${describe(err)}`
|
|
729
|
-
};
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
function launchAndroidReleaseApp({ serial, packageName }, { exec = null } = {}) {
|
|
733
|
-
const e = exec || getExecutor();
|
|
734
|
-
const component = resolveLaunchActivity(serial, packageName, { exec: e });
|
|
735
|
-
if (component) try {
|
|
736
|
-
e.runFile("adb", [
|
|
737
|
-
"-s",
|
|
738
|
-
serial,
|
|
739
|
-
"shell",
|
|
740
|
-
"am",
|
|
741
|
-
"start",
|
|
742
|
-
"-n",
|
|
743
|
-
component
|
|
744
|
-
]);
|
|
745
|
-
return {
|
|
746
|
-
ok: true,
|
|
747
|
-
mode: "am-start",
|
|
748
|
-
component
|
|
749
|
-
};
|
|
750
|
-
} catch (err) {
|
|
751
|
-
return {
|
|
752
|
-
failed: true,
|
|
753
|
-
code: LAUNCH_ERROR,
|
|
754
|
-
reason: `am start -n ${component} failed on ${serial}: ${describe(err)}`
|
|
755
|
-
};
|
|
756
|
-
}
|
|
757
|
-
try {
|
|
758
|
-
e.runFile("adb", [
|
|
759
|
-
"-s",
|
|
760
|
-
serial,
|
|
761
|
-
"shell",
|
|
762
|
-
"monkey",
|
|
763
|
-
"-p",
|
|
764
|
-
packageName,
|
|
765
|
-
"1"
|
|
766
|
-
]);
|
|
767
|
-
return {
|
|
768
|
-
ok: true,
|
|
769
|
-
mode: "monkey"
|
|
770
|
-
};
|
|
771
|
-
} catch (err) {
|
|
772
|
-
return {
|
|
773
|
-
failed: true,
|
|
774
|
-
code: LAUNCH_ERROR,
|
|
775
|
-
reason: `Could not launch ${packageName} on ${serial}: no launcher activity resolved and monkey failed: ${describe(err)}`
|
|
776
|
-
};
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
function describe(err) {
|
|
780
|
-
const e = err;
|
|
781
|
-
const stderr = e?.stderr ? String(e.stderr).trim() : "";
|
|
782
|
-
const message = e?.message ? String(e.message).trim() : String(err);
|
|
783
|
-
return stderr ? `${message}: ${stderr}` : message;
|
|
784
|
-
}
|
|
785
|
-
const RELEASE_VERIFY_WAIT_MS = 3e3;
|
|
786
|
-
async function verifyReleaseLaunch({ pid, waitMs = RELEASE_VERIFY_WAIT_MS, alive = isProcessAlive, now = Date.now, sleep = (ms) => new Promise((r) => setTimeout(r, ms)) }) {
|
|
787
|
-
const startedAt = now();
|
|
788
|
-
if (!pid) return {
|
|
789
|
-
verified: false,
|
|
790
|
-
reason: "no-pid",
|
|
791
|
-
waitedMs: 0
|
|
792
|
-
};
|
|
793
|
-
await sleep(Math.max(0, waitMs));
|
|
794
|
-
const waitedMs = now() - startedAt;
|
|
795
|
-
return alive(pid) ? {
|
|
796
|
-
verified: true,
|
|
797
|
-
waitedMs
|
|
798
|
-
} : {
|
|
799
|
-
verified: false,
|
|
800
|
-
reason: "exited",
|
|
801
|
-
waitedMs
|
|
802
|
-
};
|
|
803
|
-
}
|
|
804
|
-
function parsePidof(text) {
|
|
805
|
-
const first = String(text ?? "").trim().split(/\s+/)[0];
|
|
806
|
-
const pid = Number(first);
|
|
807
|
-
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
808
|
-
}
|
|
809
|
-
function parsePsPid(text, packageName) {
|
|
810
|
-
for (const raw of String(text ?? "").split("\n")) {
|
|
811
|
-
const cols = raw.trim().split(/\s+/);
|
|
812
|
-
if (cols.length < 2) continue;
|
|
813
|
-
if (cols[cols.length - 1] !== packageName) continue;
|
|
814
|
-
const pid = Number(cols[1]);
|
|
815
|
-
if (Number.isFinite(pid) && pid > 0) return pid;
|
|
816
|
-
}
|
|
817
|
-
return null;
|
|
818
|
-
}
|
|
819
|
-
function androidAppProcess(serial, packageName, { exec = null } = {}) {
|
|
820
|
-
const e = exec || getExecutor();
|
|
821
|
-
try {
|
|
822
|
-
const pid = parsePidof(e.runFile("adb", [
|
|
823
|
-
"-s",
|
|
824
|
-
serial,
|
|
825
|
-
"shell",
|
|
826
|
-
"pidof",
|
|
827
|
-
packageName
|
|
828
|
-
]));
|
|
829
|
-
if (pid !== null) return pid;
|
|
830
|
-
} catch {}
|
|
831
|
-
try {
|
|
832
|
-
return parsePsPid(e.runFile("adb", [
|
|
833
|
-
"-s",
|
|
834
|
-
serial,
|
|
835
|
-
"shell",
|
|
836
|
-
"ps",
|
|
837
|
-
"-A"
|
|
838
|
-
]), packageName);
|
|
839
|
-
} catch {
|
|
840
|
-
return;
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
async function verifyAndroidReleaseLaunch({ serial, packageName, waitMs = RELEASE_VERIFY_WAIT_MS, exec = null, now = Date.now, sleep = (ms) => new Promise((r) => setTimeout(r, ms)) }) {
|
|
844
|
-
const e = exec || getExecutor();
|
|
845
|
-
const startedAt = now();
|
|
846
|
-
await sleep(Math.max(0, waitMs));
|
|
847
|
-
const pid = androidAppProcess(serial, packageName, { exec: e });
|
|
848
|
-
const waitedMs = now() - startedAt;
|
|
849
|
-
if (pid === void 0) return {
|
|
850
|
-
verified: false,
|
|
851
|
-
reason: "probe-failed",
|
|
852
|
-
waitedMs
|
|
853
|
-
};
|
|
854
|
-
return pid === null ? {
|
|
855
|
-
verified: false,
|
|
856
|
-
reason: "exited",
|
|
857
|
-
waitedMs,
|
|
858
|
-
pid: null
|
|
859
|
-
} : {
|
|
860
|
-
verified: true,
|
|
861
|
-
waitedMs,
|
|
862
|
-
pid
|
|
863
|
-
};
|
|
864
|
-
}
|
|
865
|
-
function isProcessAlive(pid) {
|
|
866
|
-
try {
|
|
867
|
-
process.kill(pid, 0);
|
|
868
|
-
return true;
|
|
869
|
-
} catch (err) {
|
|
870
|
-
return err?.code === "EPERM";
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
const LAUNCH_UNVERIFIED = "unverified";
|
|
874
|
-
const LAUNCH_BUNDLING = "bundling";
|
|
875
|
-
const LAUNCH_FATAL = "fatal";
|
|
876
|
-
const VERIFY_TIMEOUT_MS = 2e4;
|
|
877
|
-
const STABILITY_WINDOW_MS = 3e3;
|
|
878
|
-
const VERIFY_POLL_MS = 500;
|
|
879
|
-
const BUNDLE_EVENTS = /* @__PURE__ */ new Set([
|
|
880
|
-
"bundle_build_started",
|
|
881
|
-
"bundle_build_done",
|
|
882
|
-
"bundle_build_failed",
|
|
883
|
-
"bundling_error",
|
|
884
|
-
"transformer_error",
|
|
885
|
-
"bundle_response_started",
|
|
886
|
-
"bundle_response_finished",
|
|
887
|
-
"bundle_response_failed"
|
|
888
|
-
]);
|
|
889
|
-
function isBundleProof(record, since = 0, platform = null) {
|
|
890
|
-
if (!record || typeof record !== "object") return false;
|
|
891
|
-
const rec = record;
|
|
892
|
-
const ts = Number(rec.ts);
|
|
893
|
-
if (!Number.isFinite(ts) || ts < Number(since || 0)) return false;
|
|
894
|
-
if (platform && recordPlatform(rec) !== platform) return false;
|
|
895
|
-
if (typeof rec.event === "string" && BUNDLE_EVENTS.has(rec.event)) return true;
|
|
896
|
-
if (rec.src === "metro" && typeof rec.msg === "string" && expoBundleLine(rec.msg)) return true;
|
|
897
|
-
return false;
|
|
898
|
-
}
|
|
899
|
-
const BUNDLE_URL_PATH = /\.bundle\b|\/_expo\/|expo-development-client/i;
|
|
900
|
-
function isBundleRequestProof(record, since = 0, port = null, platform = null) {
|
|
901
|
-
if (!record || typeof record !== "object" || !port) return false;
|
|
902
|
-
const rec = record;
|
|
903
|
-
const ts = Number(rec.ts);
|
|
904
|
-
if (!Number.isFinite(ts) || ts < Number(since || 0)) return false;
|
|
905
|
-
if (typeof rec.msg !== "string") return false;
|
|
906
|
-
if (rec.level === "error" || rec.level === "fatal") return false;
|
|
907
|
-
const sourcePlatform = recordPlatform(rec);
|
|
908
|
-
if (platform && sourcePlatform && sourcePlatform !== platform) return false;
|
|
909
|
-
const digits = String(port).replace(/[^0-9]/g, "");
|
|
910
|
-
if (!digits) return false;
|
|
911
|
-
if (!new RegExp(`:${digits}\\b`).test(rec.msg)) return false;
|
|
912
|
-
return BUNDLE_URL_PATH.test(rec.msg);
|
|
913
|
-
}
|
|
914
|
-
function expoBundleLine(msg) {
|
|
915
|
-
return /\bBundl(?:ing|ed)\b/.test(msg);
|
|
916
|
-
}
|
|
917
|
-
function nativeLaunchFailure({ readCrashes, since, platform, processAlive, mode, waitedMs }) {
|
|
918
|
-
if (!readCrashes) return null;
|
|
919
|
-
const crashes = readCrashes().filter((record) => record.event === "native_crash" && after(record, since) && recordCouldBelongToPlatform(record, platform));
|
|
920
|
-
return crashes.length ? {
|
|
921
|
-
verified: false,
|
|
922
|
-
fatal: true,
|
|
923
|
-
errors: crashes,
|
|
924
|
-
processAlive: processAlive?.() ?? null,
|
|
925
|
-
mode,
|
|
926
|
-
waitedMs
|
|
927
|
-
} : null;
|
|
928
|
-
}
|
|
929
|
-
async function verifyLaunch({ slot, logsDir, since, metroPort = null, platform = null, mode = null, requireBundleResponse, timeoutMs = VERIFY_TIMEOUT_MS, stabilityMs = STABILITY_WINDOW_MS, pollMs = VERIFY_POLL_MS, readRecords = null, readDeviceRecords = null, readClientRecords = null, readNativeCrashes = null, processAlive = null, onReadinessPending = null, now = Date.now, sleep = (ms) => new Promise((r) => setTimeout(r, ms)) } = {}) {
|
|
930
|
-
const read = readRecords || (() => readMetroRecords(logsDir));
|
|
931
|
-
const readDevice = readDeviceRecords || (() => readNdjson(logsDir, "device.ndjson"));
|
|
932
|
-
const readClient = readClientRecords || (() => readNdjson(logsDir, "client.ndjson"));
|
|
933
|
-
const startedAt = now();
|
|
934
|
-
const bundleDeadline = startedAt + Math.max(0, timeoutMs);
|
|
935
|
-
let proof = null;
|
|
936
|
-
let activity = null;
|
|
937
|
-
let stabilityDeadline = null;
|
|
938
|
-
let pendingAt = null;
|
|
939
|
-
let deliveryId = null;
|
|
940
|
-
let runtimeLoadingAt = null;
|
|
941
|
-
let runtimeStartedAt = null;
|
|
942
|
-
let nextCrashCheck = startedAt + 1e3;
|
|
943
|
-
while (true) {
|
|
944
|
-
const metroRecords = read().filter((record) => after(record, since) && (slot === void 0 || record.slot === slot));
|
|
945
|
-
const bundleRecords = metroRecords.filter((record) => !requireBundleResponse || String(record.event).startsWith("bundle_response_"));
|
|
946
|
-
const deviceRecords = readDevice().filter((record) => after(record, since) && (slot === void 0 || (record.slot ?? "default") === slot));
|
|
947
|
-
const clientRecords = readClient().filter((record) => after(record, since) && (slot === void 0 || record.slot === slot));
|
|
948
|
-
if (now() >= nextCrashCheck) {
|
|
949
|
-
nextCrashCheck = now() + 2e3;
|
|
950
|
-
const crash = nativeLaunchFailure({
|
|
951
|
-
readCrashes: readNativeCrashes,
|
|
952
|
-
since,
|
|
953
|
-
platform,
|
|
954
|
-
processAlive,
|
|
955
|
-
mode,
|
|
956
|
-
waitedMs: now() - startedAt
|
|
957
|
-
});
|
|
958
|
-
if (crash) return crash;
|
|
959
|
-
}
|
|
960
|
-
if (deliveryId === null) {
|
|
961
|
-
const request = bundleRecords.find((record) => record.event === "bundle_response_started" && typeof record.requestId === "string" && isBundleProof(record, since, platform));
|
|
962
|
-
if (request) {
|
|
963
|
-
deliveryId = request.requestId;
|
|
964
|
-
proof = null;
|
|
965
|
-
stabilityDeadline = null;
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
for (const record of bundleRecords) {
|
|
969
|
-
if (isBundleProof(record, since, platform)) activity = record;
|
|
970
|
-
const completed = deliveryId === null ? isBundleReadyProof(record, since, platform) : record.event === "bundle_response_finished" && record.requestId === deliveryId && isBundleProof(record, since, platform);
|
|
971
|
-
if (!proof && completed) {
|
|
972
|
-
proof = record;
|
|
973
|
-
stabilityDeadline = Number(record.ts) + Math.max(0, stabilityMs);
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
const signals = deviceRecords.filter((record) => Number(record.ts) <= now()).toSorted((a, b) => Number(a.ts) - Number(b.ts));
|
|
977
|
-
if (platform === "android" && runtimeLoadingAt === null) {
|
|
978
|
-
const loading = signals.find((record) => record.src === "device" && record.platform === "android" && /^(?:unknown:)?BridgelessReact\(\d+\)$/.test(String(record.proc)) && /^ReactHost\{\d+\}\.getOrCreateReactInstanceTask\(\): Loading JS Bundle$/.test(String(record.msg)) && (stabilityDeadline === null || Number(record.ts) <= stabilityDeadline));
|
|
979
|
-
if (loading) runtimeLoadingAt = Number(loading.ts);
|
|
980
|
-
}
|
|
981
|
-
if (runtimeLoadingAt !== null && runtimeStartedAt === null) {
|
|
982
|
-
const executing = signals.find((record) => record.src === "device" && record.platform === "android" && Number(record.ts) >= runtimeLoadingAt && (/^ReactNativeJS\(\d+\)$/.test(String(record.proc)) || appReadinessSignal(record, platform) !== null));
|
|
983
|
-
if (executing) runtimeStartedAt = Number(executing.ts);
|
|
984
|
-
}
|
|
985
|
-
const runtimeWaiting = runtimeLoadingAt !== null && runtimeStartedAt === null;
|
|
986
|
-
const completedAt = proof ? Math.max(Number(proof.ts), runtimeStartedAt ?? 0) : null;
|
|
987
|
-
if (completedAt !== null) stabilityDeadline = completedAt + Math.max(0, stabilityMs);
|
|
988
|
-
const readinessDeadline = completedAt !== null ? completedAt + APP_READINESS_TIMEOUT_MS : bundleDeadline;
|
|
989
|
-
if (pendingAt === null) {
|
|
990
|
-
const pending = signals.find((record) => appReadinessSignal(record, platform) === "pending" && (runtimeWaiting || stabilityDeadline === null || Number(record.ts) <= stabilityDeadline));
|
|
991
|
-
if (pending) {
|
|
992
|
-
pendingAt = Number(pending.ts);
|
|
993
|
-
onReadinessPending?.();
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
const ready = pendingAt !== null && signals.some((record) => Number(record.ts) >= pendingAt && Number(record.ts) <= readinessDeadline && appReadinessSignal(record, platform) === "ready");
|
|
997
|
-
const bundleErrors = bundleRecords.filter((record) => isFatalLaunchError(record, platform) || deliveryId !== null && record.event === "bundle_response_failed" && record.requestId === deliveryId && isBundleProof(record, since, platform));
|
|
998
|
-
if (bundleErrors.length) {
|
|
999
|
-
const alive = processAlive ? processAlive() : null;
|
|
1000
|
-
const fatalRecord = bundleErrors[bundleErrors.length - 1];
|
|
1001
|
-
const fatalAt = Number(fatalRecord?.ts ?? since ?? 0);
|
|
1002
|
-
return {
|
|
1003
|
-
verified: false,
|
|
1004
|
-
fatal: true,
|
|
1005
|
-
errors: metroRecords.filter((record) => after(record, fatalAt) && recordCouldBelongToPlatform(record, platform) && isLaunchError(record, platform)),
|
|
1006
|
-
processAlive: alive,
|
|
1007
|
-
record: fatalRecord,
|
|
1008
|
-
mode,
|
|
1009
|
-
waitedMs: now() - startedAt,
|
|
1010
|
-
...pendingAt !== null ? { readiness: "error" } : {}
|
|
1011
|
-
};
|
|
1012
|
-
}
|
|
1013
|
-
if (proof && stabilityDeadline !== null && (pendingAt !== null || now() >= stabilityDeadline)) {
|
|
1014
|
-
const errorSince = pendingAt === null ? Number(proof.ts ?? since ?? 0) : Number(since ?? 0);
|
|
1015
|
-
const errors = [
|
|
1016
|
-
...metroRecords.filter((record) => after(record, errorSince) && recordCouldBelongToPlatform(record, platform)),
|
|
1017
|
-
...deviceRecords.filter((record) => after(record, errorSince) && recordCouldBelongToPlatform(record, platform)),
|
|
1018
|
-
...clientRecords.filter((record) => after(record, errorSince) && recordCouldBelongToPlatform(record, platform))
|
|
1019
|
-
].filter((record) => isLaunchError(record, platform));
|
|
1020
|
-
const alive = processAlive ? processAlive() : null;
|
|
1021
|
-
const waitedMs = now() - startedAt;
|
|
1022
|
-
if (alive === false) return {
|
|
1023
|
-
verified: false,
|
|
1024
|
-
fatal: true,
|
|
1025
|
-
errors,
|
|
1026
|
-
processAlive: alive,
|
|
1027
|
-
record: proof,
|
|
1028
|
-
mode,
|
|
1029
|
-
waitedMs,
|
|
1030
|
-
...pendingAt !== null ? { readiness: "error" } : {}
|
|
1031
|
-
};
|
|
1032
|
-
const actionableErrors = errors.filter((record) => !isIosConnectionRefusal(record, platform));
|
|
1033
|
-
const appErrors = actionableErrors.some(isAppLaunchError);
|
|
1034
|
-
if (!runtimeWaiting && pendingAt === null || appErrors || ready || now() >= readinessDeadline) return {
|
|
1035
|
-
verified: true,
|
|
1036
|
-
record: proof,
|
|
1037
|
-
errors: actionableErrors,
|
|
1038
|
-
processAlive: alive,
|
|
1039
|
-
mode,
|
|
1040
|
-
waitedMs,
|
|
1041
|
-
...pendingAt !== null ? { readiness: appErrors ? "error" : ready ? "ready" : "timed-out" } : {}
|
|
1042
|
-
};
|
|
1043
|
-
}
|
|
1044
|
-
if ((!proof || runtimeWaiting) && now() >= bundleDeadline) {
|
|
1045
|
-
const requested = findBundleRequest(deviceRecords, since, metroPort, platform) ?? activity;
|
|
1046
|
-
const waitedMs = now() - startedAt;
|
|
1047
|
-
if (requested) return {
|
|
1048
|
-
verified: false,
|
|
1049
|
-
timedOut: true,
|
|
1050
|
-
requested: true,
|
|
1051
|
-
record: requested,
|
|
1052
|
-
mode,
|
|
1053
|
-
waitedMs
|
|
1054
|
-
};
|
|
1055
|
-
return {
|
|
1056
|
-
verified: false,
|
|
1057
|
-
timedOut: true,
|
|
1058
|
-
mode,
|
|
1059
|
-
waitedMs
|
|
1060
|
-
};
|
|
1061
|
-
}
|
|
1062
|
-
await sleep(Math.min(pollMs, Math.max(0, (runtimeWaiting ? bundleDeadline : proof && pendingAt !== null ? readinessDeadline : stabilityDeadline ?? bundleDeadline) - now())));
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
function after(record, since) {
|
|
1066
|
-
const ts = Number(record.ts);
|
|
1067
|
-
return Number.isFinite(ts) && ts >= Number(since ?? 0);
|
|
1068
|
-
}
|
|
1069
|
-
function isLaunchError(record, platform) {
|
|
1070
|
-
return record.level === "error" || record.level === "fatal" || isFatalLaunchError(record, platform);
|
|
1071
|
-
}
|
|
1072
|
-
const TCP_REFUSAL = /^TCP Conn 0x[0-9a-f]+ Failed : error 0:61 \[61\]$/i;
|
|
1073
|
-
function isIosConnectionRefusal(record, platform) {
|
|
1074
|
-
return platform === "ios" && record.src === "device" && record.level === "error" && typeof record.msg === "string" && TCP_REFUSAL.test(record.msg);
|
|
1075
|
-
}
|
|
1076
|
-
function isFatalLaunchError(record, platform) {
|
|
1077
|
-
if (platform && recordPlatform(record) !== platform) return false;
|
|
1078
|
-
return record.event === "bundle_build_failed" || record.event === "bundling_error" || record.event === "transformer_error" || (record.event === "expo_stdout" || record.event === "expo_stderr") && typeof record.msg === "string" && /\bBundling failed\b/.test(record.msg);
|
|
1079
|
-
}
|
|
1080
|
-
function isBundleReadyProof(record, since = 0, platform = null) {
|
|
1081
|
-
if (!isBundleProof(record, since, platform)) return false;
|
|
1082
|
-
const rec = record;
|
|
1083
|
-
if (rec.event === "bundle_build_done") return true;
|
|
1084
|
-
return rec.src === "metro" && typeof rec.msg === "string" && /\bBundled\b/.test(rec.msg);
|
|
1085
|
-
}
|
|
1086
|
-
function findBundleRequest(records, since, port, platform) {
|
|
1087
|
-
for (const record of records) if (isBundleRequestProof(record, since ?? 0, port, platform)) return record;
|
|
1088
|
-
return null;
|
|
1089
|
-
}
|
|
1090
|
-
function recordCouldBelongToPlatform(record, platform) {
|
|
1091
|
-
if (!platform) return true;
|
|
1092
|
-
const sourcePlatform = recordPlatform(record);
|
|
1093
|
-
return sourcePlatform === null || sourcePlatform === platform;
|
|
1094
|
-
}
|
|
1095
|
-
function recordPlatform(record) {
|
|
1096
|
-
if (record.platform === "ios" || record.platform === "android") return record.platform;
|
|
1097
|
-
if (typeof record.msg !== "string") return null;
|
|
1098
|
-
const match = record.msg.match(/\b(iOS|Android)\s+Bundl(?:ed|ing)\b/i);
|
|
1099
|
-
if (!match) return null;
|
|
1100
|
-
return match[1].toLowerCase() === "ios" ? "ios" : "android";
|
|
1101
|
-
}
|
|
1102
|
-
function readMetroRecords(logsDir) {
|
|
1103
|
-
return readNdjson(logsDir, "metro.ndjson");
|
|
1104
|
-
}
|
|
1105
|
-
function readCollectorRecords(logsDir) {
|
|
1106
|
-
return readNdjson(logsDir, "device.ndjson");
|
|
1107
|
-
}
|
|
1108
|
-
function readNdjson(logsDir, name) {
|
|
1109
|
-
if (!logsDir) return [];
|
|
1110
|
-
try {
|
|
1111
|
-
return parseNdjsonText(readFileSync(join(logsDir, name), "utf-8"));
|
|
1112
|
-
} catch {
|
|
1113
|
-
return [];
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
function unverifiedLaunchLines({ platform, metroPort, waitedMs = VERIFY_TIMEOUT_MS, bundleId = null, udid = null, serial = null, devClientUrl: url = null, mode = null, remote = false, physical = false, devClient = false, lanOrigin = null, metroOrigin = null, localNetworkPending = false, component = null }) {
|
|
1117
|
-
const seconds = Math.round(Number(waitedMs || 0) / 1e3);
|
|
1118
|
-
const localNetwork = localNetworkPending && platform === "ios" && physical;
|
|
1119
|
-
const lines = [`The app was started, but nothing fetched a bundle from this workspace's Metro (port ${metroPort}) within ${seconds}s.`];
|
|
1120
|
-
const origin = metroOrigin || `http://localhost:${metroPort}`;
|
|
1121
|
-
const picker = `If expo-dev-launcher's DEVELOPMENT SERVERS picker is showing, tap the entry for ${origin} -- NOT another workspace's, which would load a different project's bundle onto this device.`;
|
|
1122
|
-
let step = 0;
|
|
1123
|
-
const push = (text) => lines.push(` ${++step}. ${text}`);
|
|
1124
|
-
lines.push(localNetwork ? `THE PHONE'S LOCAL NETWORK PERMISSION IS NOT GRANTED: the LAN connections this launch made failed with NSURLErrorDomain -1009 and the path reason "unsatisfied (Local network prohibited)", which is what iOS returns while the "would like to find and connect to devices on your local network" prompt is unanswered OR was answered Don't Allow earlier -- the log reads the same either way. Nothing the app sends reaches ${lanOrigin || origin} until it is granted. Do this, in order:` : "The app is launched; what is unproven is that it is talking to THIS dev server. Do this, in order:");
|
|
1125
|
-
if (remote) {
|
|
1126
|
-
push(`Check that ${origin} is reachable FROM THE DEVICE's network, not just from this machine. That is the usual cause: a tunnel that stopped, or one this machine can reach and the device cannot.`);
|
|
1127
|
-
push("If an \"Open in <app>?\" alert or the expo-dev-launcher picker is showing on the remote device, confirm it with your device tool (`agent-device snapshot -i`, then `agent-device press`).");
|
|
1128
|
-
lines.push(`Then check \`stim logs --source metro\`${mode ? ` (${mode})` : ""} for a bundle request.`);
|
|
1129
|
-
return lines;
|
|
1130
|
-
}
|
|
1131
|
-
if (platform === "ios" && physical) {
|
|
1132
|
-
const target = lanOrigin || origin;
|
|
1133
|
-
const relaunch = `xcrun devicectl device process launch --device ${udid} --terminate-existing ` + (url ? `--payload-url '${url}' ` : "") + bundleId + (url ? ` -- ${DEV_MENU_LAUNCH_ARGS.join(" ")}` : "");
|
|
1134
|
-
if (localNetwork) {
|
|
1135
|
-
push(`See the prompt: agent-device alert get --platform ios --udid ${udid}. It reads the alert without opening anything, so it works while the app sits behind it.`);
|
|
1136
|
-
push(`Tap Allow: agent-device alert accept --platform ios --udid ${udid}. A second \`alert get\` then finds none.`);
|
|
1137
|
-
push("If the FIRST `alert get` already finds no alert, this permission was denied on an earlier run -- a Don't Allow persists across upgrade installs. Turn the app on by hand under Settings > Privacy & Security > Local Network; there is no API for that switch.");
|
|
1138
|
-
if (devClient) {
|
|
1139
|
-
push(`The app does NOT retry after the grant -- it stays on "Failed to load app ... The Internet connection appears to be offline." with a Reload button. Press it: agent-device snapshot -i --platform ios --udid ${udid}, then agent-device press 'label="Reload"' --platform ios --udid ${udid}. This launch carried -EXDevMenuShowsAtLaunch 0, so the Expo dev menu is not over the app; if the app was started another way and the menu is on screen, agent-device press 'label="Close"' --platform ios --udid ${udid} dismisses it.`);
|
|
1140
|
-
push(`Without agent-device, relaunching also recovers: ${relaunch}. It costs the device log -- it replaces the process the collector follows, so \`stim logs --source device\` stops for the rest of this run. Pressing Reload keeps the collector alive.`);
|
|
1141
|
-
push("By hand: tap Allow on the phone, then tap Reload on the app.");
|
|
1142
|
-
} else {
|
|
1143
|
-
push(`The app does NOT retry after the grant. A bare app is expected to show React Native's "Could not connect to development server" RedBox -- NOT VERIFIED ON HARDWARE, so read the screen rather than trusting that: agent-device snapshot -i --platform ios --udid ${udid}, then press its Reload button by the ref or label that snapshot reports.`);
|
|
1144
|
-
push(`Cleanest for a bare app: relaunch, which re-reads ip.txt: ${relaunch}. It costs the device log -- it replaces the process the collector follows, so \`stim logs --source device\` stops for the rest of this run.`);
|
|
1145
|
-
push("By hand: tap Allow on the phone, then tap Reload on the RedBox.");
|
|
1146
|
-
}
|
|
1147
|
-
lines.push("`agent-device metro reload` does NOT recover either screen: it only reaches an app already connected to Metro's websocket, and this app never connected.");
|
|
1148
|
-
lines.push(`Then check \`stim logs --source metro\`${mode ? ` (${mode})` : ""} for a bundle request.`);
|
|
1149
|
-
return lines;
|
|
1150
|
-
}
|
|
1151
|
-
push(`Tap Allow on the phone's "would like to find and connect to devices on your local network" prompt if it is showing, or turn the app on under Settings > Privacy & Security > Local Network. iOS gates every LAN connection behind that permission, it cannot be PRE-granted from this machine -- but once the prompt is up a device tool can accept it (agent-device alert get, then agent-device alert accept, both --platform ios --udid ${udid}) -- and until it is granted nothing the app sends reaches ${target}.`);
|
|
1152
|
-
push("Check the phone is on the same network as this Mac -- the same Wi-Fi SSID, not cellular, not a VPN -- and that the network does not isolate clients from each other.");
|
|
1153
|
-
push("Check macOS is not blocking inbound connections: System Settings > Network > Firewall, or `/usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate`. The gate on this machine passes either way, because macOS routes a host connection to its own address over loopback.");
|
|
1154
|
-
push(`If this Mac has several network interfaces, ${target} may not be the one the phone shares: set ios.lanHost in .stim.json to the address it can reach.`);
|
|
1155
|
-
if (devClient) {
|
|
1156
|
-
push(picker);
|
|
1157
|
-
if (url && udid) push(`Retry the deep link: xcrun devicectl device process launch --device ${udid} --terminate-existing --payload-url '${url}' ${bundleId} -- ${DEV_MENU_LAUNCH_ARGS.join(" ")}`);
|
|
1158
|
-
} else lines.push("AND READ THIS: a Debug device build with no dev client carries the JS bundle baked in when the artifact was built, so an unreachable Metro is silent. The app on screen is not broken -- it is running THAT bundle, which on a cache hit is another workspace's JS, not this workspace's.");
|
|
1159
|
-
lines.push(`Then check \`stim logs --source metro\`${mode ? ` (${mode})` : ""} for a bundle request.`);
|
|
1160
|
-
return lines;
|
|
1161
|
-
}
|
|
1162
|
-
if (platform === "ios") {
|
|
1163
|
-
if (devClient) push(picker);
|
|
1164
|
-
if (url && udid) push(`Retry the deep link: xcrun simctl openurl ${udid} '${url}'`);
|
|
1165
|
-
else if (udid && bundleId) push(`Re-launch: xcrun simctl launch --terminate-running-process ${udid} ${bundleId}`);
|
|
1166
|
-
} else {
|
|
1167
|
-
if (url && serial) push(`Re-send the dev-client deep link -- this is the command that points the app at THIS workspace's Metro: adb -s ${serial} shell am start -a android.intent.action.VIEW -d ${deviceShellArg(deviceShellArg(url))} --ez ${ANDROID_DISABLE_AUTO_LAUNCH_EXTRA} true`);
|
|
1168
|
-
if (devClient) push(picker);
|
|
1169
|
-
if (serial && bundleId) push(`If the app is stuck, restart its process: ${component ? `adb -s ${serial} shell am force-stop ${bundleId} && adb -s ${serial} shell am start -n ${component}` : url ? `adb -s ${serial} shell am force-stop ${bundleId} && adb -s ${serial} shell am start -a android.intent.action.VIEW -d ${deviceShellArg(deviceShellArg(url))} --ez ${ANDROID_DISABLE_AUTO_LAUNCH_EXTRA} true` : `adb -s ${serial} shell am force-stop ${bundleId} && adb -s ${serial} shell monkey -p ${bundleId} 1`}`);
|
|
1170
|
-
}
|
|
1171
|
-
lines.push(`Then check \`stim logs --source metro\`${mode ? ` (${mode})` : ""} for a bundle request.`);
|
|
1172
|
-
return lines;
|
|
1173
|
-
}
|
|
1174
|
-
//#endregion
|
|
1175
|
-
export { APP_READINESS_TIMEOUT_MS as A, launchIosApp as C, verifyAndroidReleaseLaunch as D, unverifiedLaunchLines as E, verifyLaunch as O, launchAndroidReleaseApp as S, readMetroRecords as T, installConflictKind as _, LAUNCH_ERROR as a, isBundleProof as b, RELEASE_VERIFY_WAIT_MS as c, androidDevClientUrl as d, clearOtherUserApps as f, installAndroidApp as g, deviceShellArg as h, LAUNCH_BUNDLING as i, verifyReleaseLaunch as k, VERIFY_TIMEOUT_MS as l, devClientUrl as m, DEFAULT_METRO_PORT as n, LAUNCH_FATAL as o, devClientDeepLink as p, INSTALL_ERROR as r, LAUNCH_UNVERIFIED as s, ADB_INSTALL_TIMEOUT_MS as t, androidAppProcess as u, installIosApp as v, readCollectorRecords as w, launchAndroidApp as x, iosAppProcess as y };
|