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,528 +1,84 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
function dynamicConfigFile(root) {
|
|
31
|
-
for (const name of DYNAMIC_CONFIG_FILES) if (existsSync(join(root, name))) return name;
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
function providerFromConfig(config) {
|
|
35
|
-
if (!config || typeof config !== "object") return null;
|
|
36
|
-
const expoField = config.expo;
|
|
37
|
-
const exp = expoField && typeof expoField === "object" ? expoField : config;
|
|
38
|
-
return exp.buildCacheProvider ?? exp.experiments?.buildCacheProvider ?? null;
|
|
39
|
-
}
|
|
40
|
-
function ownerFromConfig(config) {
|
|
41
|
-
if (!config || typeof config !== "object") return null;
|
|
42
|
-
const expoField = config.expo;
|
|
43
|
-
const owner = (expoField && typeof expoField === "object" ? expoField : config).owner;
|
|
44
|
-
return typeof owner === "string" && owner.trim() !== "" ? owner.trim() : null;
|
|
45
|
-
}
|
|
46
|
-
function normalizeProvider(raw) {
|
|
47
|
-
if (raw === null || raw === void 0 || raw === "") return null;
|
|
48
|
-
if (raw === "eas") return {
|
|
49
|
-
name: "eas",
|
|
50
|
-
reference: EAS_PROVIDER_PACKAGE,
|
|
51
|
-
options: {}
|
|
52
|
-
};
|
|
53
|
-
if (typeof raw === "object") {
|
|
54
|
-
const obj = raw;
|
|
55
|
-
if (typeof obj.plugin === "string" && obj.plugin.trim() !== "") {
|
|
56
|
-
const reference = obj.plugin.trim();
|
|
57
|
-
return {
|
|
58
|
-
name: reference,
|
|
59
|
-
reference,
|
|
60
|
-
options: obj.options && typeof obj.options === "object" ? obj.options : {}
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return { invalid: typeof raw === "string" ? `"${raw}"` : JSON.stringify(raw) };
|
|
65
|
-
}
|
|
66
|
-
function runOptionsFor(platform) {
|
|
67
|
-
return platform === "android" ? { variant: "debug" } : { configuration: "Debug" };
|
|
68
|
-
}
|
|
69
|
-
function isProviderPlugin(plugin) {
|
|
70
|
-
if (!plugin || typeof plugin !== "object") return false;
|
|
71
|
-
const p = plugin;
|
|
72
|
-
const resolves = typeof p.resolveBuildCache === "function" || typeof p.resolveRemoteBuildCache === "function";
|
|
73
|
-
const uploads = typeof p.uploadBuildCache === "function" || typeof p.uploadRemoteBuildCache === "function";
|
|
74
|
-
return resolves && uploads;
|
|
75
|
-
}
|
|
76
|
-
function cacheLevel(value) {
|
|
77
|
-
return value === "local" || value === "remote" ? value : false;
|
|
78
|
-
}
|
|
79
|
-
function exitAfterFlush(code = 0, { exit = (c) => process.exit(c), stream = process.stdout } = {}) {
|
|
80
|
-
try {
|
|
81
|
-
stream.write("", () => exit(code));
|
|
82
|
-
} catch {
|
|
83
|
-
exit(code);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
function readProjectConfig(root, { run = null, timeoutMs = CONFIG_TIMEOUT_MS } = {}) {
|
|
87
|
-
const dynamic = dynamicConfigFile(root);
|
|
88
|
-
if (!dynamic) {
|
|
89
|
-
const file = join(root, "app.json");
|
|
90
|
-
if (!existsSync(file)) return {
|
|
91
|
-
config: null,
|
|
92
|
-
source: null
|
|
93
|
-
};
|
|
94
|
-
try {
|
|
95
|
-
return {
|
|
96
|
-
config: JSON.parse(readFileSync(file, "utf-8")),
|
|
97
|
-
source: "app.json"
|
|
98
|
-
};
|
|
99
|
-
} catch (err) {
|
|
100
|
-
return { unavailable: `app.json could not be parsed: ${firstLine(err)}` };
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
const bin = expoBinPath(root);
|
|
104
|
-
if (!bin) return { unavailable: `${dynamic} is code, so it has to be evaluated to read its buildCacheProvider, and the \`expo\` package is not resolvable from this project (no expo/package.json to read a bin from, and no node_modules/.bin/expo here or in any parent)` };
|
|
105
|
-
const exec = run || ((file, args, opts) => getExecutor().runFile(file, args, opts));
|
|
106
|
-
let stdout;
|
|
107
|
-
try {
|
|
108
|
-
stdout = exec(bin, [
|
|
109
|
-
"config",
|
|
110
|
-
"--json",
|
|
111
|
-
root
|
|
112
|
-
], { timeoutMs });
|
|
113
|
-
} catch (err) {
|
|
114
|
-
return { unavailable: `\`expo config --json\` failed: ${firstLine(err)}` };
|
|
115
|
-
}
|
|
116
|
-
try {
|
|
117
|
-
return {
|
|
118
|
-
config: JSON.parse(String(stdout)),
|
|
119
|
-
source: dynamic
|
|
120
|
-
};
|
|
121
|
-
} catch (err) {
|
|
122
|
-
return { unavailable: `\`expo config --json\` did not print JSON: ${firstLine(err)}` };
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
async function loadPlugin(projectRoot, reference, { requireFrom = null } = {}) {
|
|
126
|
-
const localRequire = requireFrom ? requireFrom(projectRoot) : createRequire(join(projectRoot, "package.json"));
|
|
127
|
-
const file = localRequire.resolve(reference);
|
|
128
|
-
let mod;
|
|
129
|
-
try {
|
|
130
|
-
mod = localRequire(file);
|
|
131
|
-
} catch (err) {
|
|
132
|
-
if (err?.code !== "ERR_REQUIRE_ESM") throw err;
|
|
133
|
-
mod = await import(pathToFileURL(file).href);
|
|
134
|
-
}
|
|
135
|
-
const plugin = mod?.default ?? mod;
|
|
136
|
-
if (!isProviderPlugin(plugin)) throw new Error(`"${reference}" does not export resolveBuildCache and uploadBuildCache functions`);
|
|
137
|
-
return plugin;
|
|
138
|
-
}
|
|
139
|
-
async function loadProjectProvider(projectRoot, { isExpo = true, run = null, requireFrom = null, timeoutMs = CONFIG_TIMEOUT_MS } = {}) {
|
|
140
|
-
if (!isExpo) return { none: true };
|
|
141
|
-
const read = readProjectConfig(projectRoot, {
|
|
142
|
-
run,
|
|
143
|
-
timeoutMs
|
|
144
|
-
});
|
|
145
|
-
if (read.unavailable) return { unavailable: read.unavailable };
|
|
146
|
-
const normalized = normalizeProvider(providerFromConfig(read.config));
|
|
147
|
-
if (!normalized) return { none: true };
|
|
148
|
-
if (normalized.invalid) return { unavailable: `buildCacheProvider is ${normalized.invalid}, which is not "eas" or { plugin: <module> }` };
|
|
149
|
-
if (normalized.reference === "@stim-cli/expo-build-cache") return { none: true };
|
|
150
|
-
let plugin;
|
|
151
|
-
try {
|
|
152
|
-
plugin = await loadPlugin(projectRoot, normalized.reference, { requireFrom });
|
|
153
|
-
} catch (err) {
|
|
154
|
-
const missing = err?.code === "MODULE_NOT_FOUND";
|
|
155
|
-
return { unavailable: normalized.name === "eas" && missing ? `the EAS build cache needs the \`${EAS_PROVIDER_PACKAGE}\` package, which is not installed in this project` : `${normalized.name} could not be loaded: ${firstLine(err)}` };
|
|
156
|
-
}
|
|
157
|
-
return {
|
|
158
|
-
provider: {
|
|
159
|
-
plugin,
|
|
160
|
-
options: normalized.options ?? {}
|
|
161
|
-
},
|
|
162
|
-
name: normalized.name,
|
|
163
|
-
owner: ownerFromConfig(read.config)
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
const WHOAMI_TIMEOUT_MS = 15e3;
|
|
167
|
-
const EAS_CLI_PACKAGE = "eas-cli";
|
|
168
|
-
const EAS_CLI_BIN = "eas";
|
|
169
|
-
function parseWhoami({ stdout = "", stderr = "", exitCode = 0 } = {}) {
|
|
170
|
-
const out = stripAnsi(stdout);
|
|
171
|
-
const combined = `${out}\n${stripAnsi(stderr)}`;
|
|
172
|
-
if (/^\s*Not logged in\s*$/m.test(combined)) return { loggedOut: true };
|
|
173
|
-
if (exitCode !== 0) return { unknown: firstLine(combined.trim() || `eas whoami exited ${exitCode}`) };
|
|
174
|
-
const lines = out.split("\n").map((line) => line.trim()).filter((line) => line !== "");
|
|
175
|
-
if (lines.length === 0) return { unknown: "eas whoami printed nothing" };
|
|
176
|
-
const raw = lines[0];
|
|
177
|
-
if (raw === void 0) return { unknown: "eas whoami printed nothing" };
|
|
178
|
-
const viaToken = /\(authenticated using EXPO_TOKEN\)\s*$/.test(raw);
|
|
179
|
-
const display = raw.replace(/\s*\(authenticated using EXPO_TOKEN\)\s*$/, "").trim();
|
|
180
|
-
const accounts = [];
|
|
181
|
-
for (const line of lines) {
|
|
182
|
-
const name = /^\u2022\s*(\S+)\s*\(Role:/.exec(line)?.[1];
|
|
183
|
-
if (name !== void 0) accounts.push(name);
|
|
184
|
-
}
|
|
185
|
-
if (accounts.length === 0 && !display.endsWith("(robot)") && display !== "robot") accounts.push(display);
|
|
186
|
-
return {
|
|
187
|
-
loggedIn: true,
|
|
188
|
-
account: display,
|
|
189
|
-
accounts: accounts.length ? accounts : null,
|
|
190
|
-
viaToken
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
function isEasAuthFailureText(text) {
|
|
194
|
-
const t = stripAnsi(text || "");
|
|
195
|
-
if (t.trim() === "") return false;
|
|
196
|
-
return /not logged in/i.test(t) || /\beas login\b/i.test(t) || /EXPO_TOKEN/.test(t) || /unauthorized/i.test(t) || /not authorized/i.test(t) || /authentication (failed|required)/i.test(t);
|
|
197
|
-
}
|
|
198
|
-
function easAuthNote(status) {
|
|
199
|
-
const { code, account, owner, reason, phase = "resolve" } = status || {};
|
|
200
|
-
const because = reason ? ` (${reason})` : "";
|
|
201
|
-
if (code === "logged-out") return `eas is not authenticated${because} -- run \`eas login\` (or set EXPO_TOKEN); ` + (phase === "upload" ? "this build stayed in the local cache" : "building with the local cache only");
|
|
202
|
-
if (code === "wrong-account") return `eas is authenticated as ${account}, and this project's owner is ${owner} -- run \`eas login\` as a member of ${owner} (or set EXPO_TOKEN); consulting the EAS cache anyway`;
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
205
|
-
function resolveEasCliBin(projectRoot, { lookupPath = null } = {}) {
|
|
206
|
-
const fromPackage = expoBinFromPackage(resolvePackageJson(projectRoot, EAS_CLI_PACKAGE), EAS_CLI_BIN);
|
|
207
|
-
if (fromPackage) return {
|
|
208
|
-
file: fromPackage,
|
|
209
|
-
source: "project"
|
|
210
|
-
};
|
|
211
|
-
const shim = findBinUpward(projectRoot, EAS_CLI_BIN);
|
|
212
|
-
if (shim) return {
|
|
213
|
-
file: shim,
|
|
214
|
-
source: "project"
|
|
215
|
-
};
|
|
216
|
-
const onPath = lookupPath ? lookupPath() : getExecutor().findExecutable(EAS_CLI_BIN);
|
|
217
|
-
const file = (String(onPath || "").split("\n")[0] ?? "").trim();
|
|
218
|
-
return file ? {
|
|
219
|
-
file,
|
|
220
|
-
source: "path"
|
|
221
|
-
} : null;
|
|
222
|
-
}
|
|
223
|
-
const easAuthCache = /* @__PURE__ */ new Map();
|
|
224
|
-
function checkEasAuth({ projectRoot, owner = null, run = null, resolveBin = null, timeoutMs = WHOAMI_TIMEOUT_MS, cache = easAuthCache }) {
|
|
225
|
-
const key = `${projectRoot}::${owner || ""}`;
|
|
226
|
-
if (cache?.has(key)) return cache.get(key);
|
|
227
|
-
const answer = probeEasAuth({
|
|
228
|
-
projectRoot,
|
|
229
|
-
owner,
|
|
230
|
-
run,
|
|
231
|
-
resolveBin,
|
|
232
|
-
timeoutMs
|
|
233
|
-
});
|
|
234
|
-
cache?.set(key, answer);
|
|
235
|
-
return answer;
|
|
236
|
-
}
|
|
237
|
-
function probeEasAuth({ projectRoot, owner, run, resolveBin, timeoutMs }) {
|
|
238
|
-
const bin = (resolveBin || resolveEasCliBin)(projectRoot);
|
|
239
|
-
if (!bin) return {
|
|
240
|
-
failed: true,
|
|
241
|
-
code: "no-cli",
|
|
242
|
-
reason: "no `eas` executable is resolvable from this project or on PATH",
|
|
243
|
-
remedy: "Install eas-cli (`npm i -g eas-cli`, or as a project devDependency), then run `eas login`."
|
|
244
|
-
};
|
|
245
|
-
const exec = run || ((file, args, opts) => getExecutor().runFile(file, args, opts));
|
|
246
|
-
let result;
|
|
247
|
-
try {
|
|
248
|
-
result = {
|
|
249
|
-
stdout: String(exec(bin.file, ["whoami"], { timeoutMs }) ?? ""),
|
|
250
|
-
exitCode: 0
|
|
251
|
-
};
|
|
252
|
-
} catch (e) {
|
|
253
|
-
const err = e;
|
|
254
|
-
if (err?.code === "ETIMEDOUT" || err?.signal === "SIGTERM" || err?.killed) return { unknown: `eas whoami timed out after ${timeoutMs}ms` };
|
|
255
|
-
result = {
|
|
256
|
-
stdout: String(err?.stdout ?? ""),
|
|
257
|
-
stderr: String(err?.stderr ?? err?.message ?? ""),
|
|
258
|
-
exitCode: typeof err?.status === "number" ? err.status : 1
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
const parsed = parseWhoami(result);
|
|
262
|
-
if (parsed.loggedOut) return {
|
|
263
|
-
failed: true,
|
|
264
|
-
code: "logged-out",
|
|
265
|
-
reason: "eas whoami says \"Not logged in\"",
|
|
266
|
-
remedy: "Run `eas login` (or set EXPO_TOKEN)."
|
|
267
|
-
};
|
|
268
|
-
if (parsed.unknown) return { unknown: parsed.unknown };
|
|
269
|
-
if (owner && Array.isArray(parsed.accounts) && !parsed.accounts.includes(owner)) return {
|
|
270
|
-
failed: true,
|
|
271
|
-
code: "wrong-account",
|
|
272
|
-
account: parsed.account,
|
|
273
|
-
accounts: parsed.accounts,
|
|
274
|
-
owner,
|
|
275
|
-
reason: `eas is authenticated as ${parsed.account} (accounts: ${parsed.accounts.join(", ")}), which does not include this project's owner ${owner}`,
|
|
276
|
-
remedy: `Run \`eas login\` as a member of ${owner}, or set EXPO_TOKEN to a token for it.`
|
|
277
|
-
};
|
|
278
|
-
return {
|
|
279
|
-
ok: true,
|
|
280
|
-
account: parsed.account,
|
|
281
|
-
accounts: parsed.accounts,
|
|
282
|
-
viaToken: parsed.viaToken,
|
|
283
|
-
source: bin.source
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
const capturing = [];
|
|
287
|
-
const abandoned = /* @__PURE__ */ new Set();
|
|
288
|
-
let patched = null;
|
|
289
|
-
const PROVIDER_CONTEXT = new AsyncLocalStorage();
|
|
290
|
-
function uploadDestination(lines) {
|
|
291
|
-
const text = (lines || []).map(stripAnsi);
|
|
292
|
-
for (const line of text) {
|
|
293
|
-
const url = /(https?:\/\/[^\s'"]+)/.exec(line)?.[1];
|
|
294
|
-
if (url !== void 0) return url.replace(/[.,)\]]+$/, "");
|
|
295
|
-
}
|
|
296
|
-
for (const line of text) {
|
|
297
|
-
const slug = /\bto\s+(@?[\w.-]+\/[\w.-]+)/i.exec(line)?.[1];
|
|
298
|
-
if (slug !== void 0) return slug.replace(/[.,)\]]+$/, "");
|
|
299
|
-
}
|
|
300
|
-
return null;
|
|
301
|
-
}
|
|
302
|
-
function providerNote(text) {
|
|
303
|
-
return `${"cache".padEnd(11)} ${text}`;
|
|
304
|
-
}
|
|
305
|
-
function currentFrame() {
|
|
306
|
-
const store = PROVIDER_CONTEXT.getStore();
|
|
307
|
-
return store && (capturing.includes(store) || abandoned.has(store)) ? store : null;
|
|
308
|
-
}
|
|
309
|
-
function install() {
|
|
310
|
-
if (patched && process.stdout.write === patched.patchedWrite) return;
|
|
311
|
-
const stdout = process.stdout;
|
|
312
|
-
const write = stdout.write.bind(stdout);
|
|
313
|
-
const log = console.log;
|
|
314
|
-
const passthrough = write;
|
|
315
|
-
const patchedWrite = ((chunk, encoding, callback) => {
|
|
316
|
-
const frame = currentFrame();
|
|
317
|
-
if (!frame) return passthrough(chunk, encoding, callback);
|
|
318
|
-
const cb = typeof encoding === "function" ? encoding : callback;
|
|
319
|
-
absorb(frame, typeof chunk === "string" ? chunk : String(chunk));
|
|
320
|
-
if (typeof cb === "function") cb();
|
|
321
|
-
return true;
|
|
1
|
+
import { D as process_identity_exports, H as projectDeviceSlots, I as assignSlotDevice, O as acquireAvdClaim, T as withConfigLock, U as removeSlotDevice, d as getConfigPath, g as loadConfig, k as ownership_claim_exports, s as ensureConfig, u as getConfigDir, x as saveConfig, z as deviceSlotPlatforms } from "./config-BEq0Qw_a.mjs";
|
|
2
|
+
import { t as getExecutor } from "./exec-DK8KaVHK.mjs";
|
|
3
|
+
import { c as signalProcessTree, i as pidExists } from "./metro-BWLOJ_Wr.mjs";
|
|
4
|
+
import { r as workspaceProcessLockPath, t as withWorkspaceProcessLock } from "./workspace-process-lock-BXA96myD.mjs";
|
|
5
|
+
import { t as createLineReader } from "./process-output-Fpl8PYi8.mjs";
|
|
6
|
+
import { l as phaseLine } from "./command-output-D5z2_bcF.mjs";
|
|
7
|
+
import { A as ownedAvdDirectory, C as isStimOwnedAvdName, D as listOrphanedAvdDirectories, G as wipeAvdUserData, H as sleepSync, P as ownedAvdSystemImage, U as waitForAndroidEmulatorShutdown, V as shutdownAndroidEmulator, f as avdStorageRoots, g as deleteAvd, j as ownedAvdMatchesConfiguration, l as assertOwnedAvdStopped, u as avdPathExists, z as resolveOwnedAvdSerial } from "./android-787HhP5w.mjs";
|
|
8
|
+
import { S as resolveOwnedIosSim, a as deleteParkedIosSim, d as listIosDeviceTypes, g as parseRuntimeVersion, h as parkedSimName, i as deleteIosSim, o as eraseIosSim, v as renameIosSim, w as shutdownIosSim } from "./ios-B2Fpqvlb.mjs";
|
|
9
|
+
import { closeSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, unlinkSync } from "node:fs";
|
|
10
|
+
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
11
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
12
|
+
import { isDeepStrictEqual } from "node:util";
|
|
13
|
+
import { resolve4 } from "node:dns/promises";
|
|
14
|
+
import { request } from "node:https";
|
|
15
|
+
//#region src/devices/agent-device-cleanup.ts
|
|
16
|
+
function parseAgentDeviceSessions(output) {
|
|
17
|
+
const result = JSON.parse(output);
|
|
18
|
+
if (result?.success !== true || !Array.isArray(result.data?.sessions)) throw new Error("agent-device did not return a successful session list");
|
|
19
|
+
return result.data.sessions.flatMap((entry) => {
|
|
20
|
+
if (!entry || typeof entry !== "object") return [];
|
|
21
|
+
const row = entry;
|
|
22
|
+
const id = row.platform === "ios" ? row.device_udid : row.id;
|
|
23
|
+
if (row.platform !== "ios" && row.platform !== "android" || typeof row.name !== "string" || !row.name.trim() || typeof id !== "string" || !id || typeof row.createdAt !== "number" || !Number.isFinite(row.createdAt) || row.platform === "ios" && row.id !== void 0 && row.id !== id) return [];
|
|
24
|
+
return [{
|
|
25
|
+
name: row.name,
|
|
26
|
+
platform: row.platform,
|
|
27
|
+
id,
|
|
28
|
+
createdAt: row.createdAt
|
|
29
|
+
}];
|
|
322
30
|
});
|
|
323
|
-
const patchedLog = (...args) => {
|
|
324
|
-
const frame = currentFrame();
|
|
325
|
-
if (!frame) return log(...args);
|
|
326
|
-
absorb(frame, `${format(...args)}\n`);
|
|
327
|
-
};
|
|
328
|
-
stdout.write = patchedWrite;
|
|
329
|
-
console.log = patchedLog;
|
|
330
|
-
patched = {
|
|
331
|
-
stdout,
|
|
332
|
-
write,
|
|
333
|
-
log,
|
|
334
|
-
patchedWrite,
|
|
335
|
-
patchedLog
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
function uninstall() {
|
|
339
|
-
if (!patched || capturing.length || abandoned.size) return;
|
|
340
|
-
if (patched.stdout.write === patched.patchedWrite) patched.stdout.write = patched.write;
|
|
341
|
-
if (console.log === patched.patchedLog) console.log = patched.log;
|
|
342
|
-
patched = null;
|
|
343
31
|
}
|
|
344
|
-
function
|
|
345
|
-
|
|
346
|
-
process.stderr.write(chunk);
|
|
347
|
-
} catch {}
|
|
348
|
-
frame.pending += chunk;
|
|
349
|
-
let index = frame.pending.indexOf("\n");
|
|
350
|
-
while (index !== -1) {
|
|
351
|
-
emitLine(frame, frame.pending.slice(0, index));
|
|
352
|
-
frame.pending = frame.pending.slice(index + 1);
|
|
353
|
-
index = frame.pending.indexOf("\n");
|
|
354
|
-
}
|
|
32
|
+
function report(message) {
|
|
33
|
+
process.stderr.write(`${phaseLine("device", message)}\n`);
|
|
355
34
|
}
|
|
356
|
-
function
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
35
|
+
function closeOwnedDeviceSessions(device, stillOwned) {
|
|
36
|
+
const exec = getExecutor();
|
|
37
|
+
const deadline = Date.now() + 15e3;
|
|
38
|
+
const run = (args) => {
|
|
39
|
+
const remaining = deadline - Date.now();
|
|
40
|
+
if (remaining <= 0) throw new Error("agent-device cleanup timed out");
|
|
41
|
+
return exec.runFile("agent-device", [
|
|
42
|
+
...args,
|
|
43
|
+
"--json",
|
|
44
|
+
"--daemon-transport",
|
|
45
|
+
"socket"
|
|
46
|
+
], {
|
|
47
|
+
timeoutMs: Math.min(5e3, remaining),
|
|
48
|
+
killSignal: "SIGKILL"
|
|
367
49
|
});
|
|
368
|
-
} catch {}
|
|
369
|
-
frame.onLine?.(line);
|
|
370
|
-
}
|
|
371
|
-
function beginCapture({ logWriter = null, projectRoot = null, platform = null, onLine = null } = {}) {
|
|
372
|
-
let own = null;
|
|
373
|
-
const frame = {
|
|
374
|
-
lines: [],
|
|
375
|
-
pending: "",
|
|
376
|
-
onLine,
|
|
377
|
-
writer: () => {
|
|
378
|
-
if (logWriter) return logWriter;
|
|
379
|
-
if (own) return own;
|
|
380
|
-
if (!projectRoot || !platform) return null;
|
|
381
|
-
own = createNdjsonWriter(join(workspaceLogsDir(projectRoot), `build-${platform}.ndjson`));
|
|
382
|
-
return own;
|
|
383
|
-
},
|
|
384
|
-
close: () => {
|
|
385
|
-
own?.close?.();
|
|
386
|
-
own = null;
|
|
387
|
-
}
|
|
388
50
|
};
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
const at = capturing.lastIndexOf(frame);
|
|
396
|
-
if (at !== -1) capturing.splice(at, 1);
|
|
397
|
-
abandoned.delete(frame);
|
|
398
|
-
if (frame.pending !== "") {
|
|
399
|
-
const rest = frame.pending;
|
|
400
|
-
frame.pending = "";
|
|
401
|
-
emitLine(frame, rest);
|
|
402
|
-
}
|
|
403
|
-
frame.close();
|
|
404
|
-
uninstall();
|
|
405
|
-
}
|
|
406
|
-
function abandonCapture(frame, work) {
|
|
407
|
-
if (!frame) return;
|
|
408
|
-
const at = capturing.lastIndexOf(frame);
|
|
409
|
-
if (at !== -1) capturing.splice(at, 1);
|
|
410
|
-
abandoned.add(frame);
|
|
411
|
-
Promise.resolve(work).then(() => endCapture(frame), () => endCapture(frame));
|
|
412
|
-
}
|
|
413
|
-
async function resolveRemote({ provider, platform, projectRoot, fingerprintHash, runOptions = null, timeoutMs = RESOLVE_TIMEOUT_MS, logWriter = null } = {}) {
|
|
414
|
-
if (!provider?.plugin || !fingerprintHash) return null;
|
|
415
|
-
const opts = runOptions || runOptionsFor(platform);
|
|
416
|
-
const outcome = await withBudget(async () => {
|
|
417
|
-
const props = {
|
|
418
|
-
fingerprintHash: await providerFingerprint({
|
|
419
|
-
provider,
|
|
420
|
-
platform,
|
|
421
|
-
projectRoot,
|
|
422
|
-
runOptions: opts
|
|
423
|
-
}) ?? fingerprintHash,
|
|
424
|
-
platform,
|
|
425
|
-
runOptions: opts,
|
|
426
|
-
projectRoot
|
|
427
|
-
};
|
|
428
|
-
return typeof provider.plugin.resolveBuildCache === "function" ? provider.plugin.resolveBuildCache(props, provider.options) : provider.plugin.resolveRemoteBuildCache(props, provider.options);
|
|
429
|
-
}, timeoutMs, {
|
|
430
|
-
logWriter,
|
|
431
|
-
projectRoot,
|
|
432
|
-
platform
|
|
433
|
-
});
|
|
434
|
-
if (outcome.timedOut) return { timedOut: true };
|
|
435
|
-
if (outcome.error) return { failed: firstLine(outcome.error) };
|
|
436
|
-
const appPath = typeof outcome.value === "string" ? outcome.value.trim() : "";
|
|
437
|
-
if (!appPath) return null;
|
|
438
|
-
if (!existsSync(appPath)) return { failed: `returned ${appPath}, which does not exist` };
|
|
439
|
-
return { appPath };
|
|
440
|
-
}
|
|
441
|
-
async function uploadRemote({ provider, platform, projectRoot, fingerprintHash, buildPath, runOptions = null, timeoutMs = UPLOAD_TIMEOUT_MS, logWriter = null, note = (line) => console.error(line) } = {}) {
|
|
442
|
-
if (!provider?.plugin || !fingerprintHash || !buildPath) return { skipped: true };
|
|
443
|
-
const opts = runOptions || runOptionsFor(platform);
|
|
444
|
-
let announced = false;
|
|
445
|
-
const onLine = (line) => {
|
|
446
|
-
if (announced) return;
|
|
447
|
-
const dest = uploadDestination([line]);
|
|
448
|
-
if (!dest) return;
|
|
449
|
-
announced = true;
|
|
450
|
-
note(providerNote(`uploading to ${dest}`));
|
|
451
|
-
};
|
|
452
|
-
const outcome = await withBudget(async () => {
|
|
453
|
-
const props = {
|
|
454
|
-
projectRoot,
|
|
455
|
-
platform,
|
|
456
|
-
fingerprintHash: await providerFingerprint({
|
|
457
|
-
provider,
|
|
458
|
-
platform,
|
|
459
|
-
projectRoot,
|
|
460
|
-
runOptions: opts
|
|
461
|
-
}) ?? fingerprintHash,
|
|
462
|
-
buildPath,
|
|
463
|
-
runOptions: opts
|
|
464
|
-
};
|
|
465
|
-
return typeof provider.plugin.uploadBuildCache === "function" ? provider.plugin.uploadBuildCache(props, provider.options) : provider.plugin.uploadRemoteBuildCache(props, provider.options);
|
|
466
|
-
}, timeoutMs, {
|
|
467
|
-
logWriter,
|
|
468
|
-
projectRoot,
|
|
469
|
-
platform,
|
|
470
|
-
onLine
|
|
471
|
-
});
|
|
472
|
-
if (outcome.timedOut) return { timedOut: true };
|
|
473
|
-
if (outcome.error) return { failed: firstLine(outcome.error) };
|
|
474
|
-
const destination = uploadDestination(outcome.lines);
|
|
475
|
-
return destination ? {
|
|
476
|
-
uploaded: true,
|
|
477
|
-
destination
|
|
478
|
-
} : { uploaded: true };
|
|
479
|
-
}
|
|
480
|
-
async function providerFingerprint({ provider, platform, projectRoot, runOptions }) {
|
|
481
|
-
if (typeof provider.plugin.calculateFingerprintHash !== "function") return null;
|
|
482
|
-
try {
|
|
483
|
-
const hash = await provider.plugin.calculateFingerprintHash({
|
|
484
|
-
projectRoot,
|
|
485
|
-
platform,
|
|
486
|
-
runOptions
|
|
487
|
-
}, provider.options);
|
|
488
|
-
return typeof hash === "string" && hash.trim() !== "" ? hash.trim() : null;
|
|
489
|
-
} catch {
|
|
490
|
-
return null;
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
async function withBudget(factory, ms, capture) {
|
|
494
|
-
let timer = null;
|
|
495
|
-
const timeout = new Promise((resolve) => {
|
|
496
|
-
timer = setTimeout(() => resolve(TIMED_OUT), ms);
|
|
497
|
-
timer?.unref?.();
|
|
498
|
-
});
|
|
499
|
-
const frame = beginCapture(capture || {});
|
|
500
|
-
const work = Promise.resolve().then(() => PROVIDER_CONTEXT.run(frame, factory)).then((value) => ({ value }), (error) => ({ error }));
|
|
51
|
+
const list = () => parseAgentDeviceSessions(run([
|
|
52
|
+
"session",
|
|
53
|
+
"list",
|
|
54
|
+
"--session",
|
|
55
|
+
"stim-teardown-inventory"
|
|
56
|
+
]));
|
|
501
57
|
try {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
58
|
+
if (!exec.findExecutable("agent-device")) return;
|
|
59
|
+
const sessions = list().filter((session) => session.platform === device.platform && session.id === device.id);
|
|
60
|
+
for (const session of sessions) try {
|
|
61
|
+
if (!list().some((current) => current.name === session.name && current.platform === session.platform && current.id === session.id && current.createdAt === session.createdAt)) continue;
|
|
62
|
+
if (!stillOwned()) return;
|
|
63
|
+
if (JSON.parse(run([
|
|
64
|
+
"close",
|
|
65
|
+
"--session",
|
|
66
|
+
session.name,
|
|
67
|
+
"--session-lock",
|
|
68
|
+
"reject",
|
|
69
|
+
"--platform",
|
|
70
|
+
device.platform,
|
|
71
|
+
device.platform === "ios" ? "--udid" : "--serial",
|
|
72
|
+
device.id
|
|
73
|
+
]))?.success !== true) throw new Error("agent-device did not confirm session close");
|
|
74
|
+
report(`closed agent-device session ${session.name} on ${device.id}`);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
report(`could not close agent-device session ${session.name} on ${device.id}: ${error.message}`);
|
|
509
77
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
...settled,
|
|
513
|
-
lines: frame.lines
|
|
514
|
-
};
|
|
515
|
-
} catch (err) {
|
|
516
|
-
endCapture(frame);
|
|
517
|
-
throw err;
|
|
518
|
-
} finally {
|
|
519
|
-
if (timer) clearTimeout(timer);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
report(`could not list agent-device sessions for ${device.id}: ${error.message}`);
|
|
520
80
|
}
|
|
521
81
|
}
|
|
522
|
-
function firstLine(err) {
|
|
523
|
-
const line = (String(err?.message || err || "unknown error").trim().split("\n")[0] ?? "").trim();
|
|
524
|
-
return line.length > 200 ? `${line.slice(0, 197)}...` : line;
|
|
525
|
-
}
|
|
526
82
|
const POOL_SETTING_REMEDY = "Run `stim guide settings` for the device pool bounds and where it can be set.";
|
|
527
83
|
const MAX_SETTING = {
|
|
528
84
|
ios: {
|
|
@@ -720,74 +276,6 @@ function dropParked(platform, udid) {
|
|
|
720
276
|
return removeParkedAfter(platform, udid, () => {}) !== null;
|
|
721
277
|
}
|
|
722
278
|
//#endregion
|
|
723
|
-
//#region src/devices/agent-device-cleanup.ts
|
|
724
|
-
function parseAgentDeviceSessions(output) {
|
|
725
|
-
const result = JSON.parse(output);
|
|
726
|
-
if (result?.success !== true || !Array.isArray(result.data?.sessions)) throw new Error("agent-device did not return a successful session list");
|
|
727
|
-
return result.data.sessions.flatMap((entry) => {
|
|
728
|
-
if (!entry || typeof entry !== "object") return [];
|
|
729
|
-
const row = entry;
|
|
730
|
-
const id = row.platform === "ios" ? row.device_udid : row.id;
|
|
731
|
-
if (row.platform !== "ios" && row.platform !== "android" || typeof row.name !== "string" || !row.name.trim() || typeof id !== "string" || !id || typeof row.createdAt !== "number" || !Number.isFinite(row.createdAt) || row.platform === "ios" && row.id !== void 0 && row.id !== id) return [];
|
|
732
|
-
return [{
|
|
733
|
-
name: row.name,
|
|
734
|
-
platform: row.platform,
|
|
735
|
-
id,
|
|
736
|
-
createdAt: row.createdAt
|
|
737
|
-
}];
|
|
738
|
-
});
|
|
739
|
-
}
|
|
740
|
-
function report(message) {
|
|
741
|
-
process.stderr.write(`${phaseLine("device", message)}\n`);
|
|
742
|
-
}
|
|
743
|
-
function closeOwnedDeviceSessions(device, stillOwned) {
|
|
744
|
-
const exec = getExecutor();
|
|
745
|
-
const deadline = Date.now() + 15e3;
|
|
746
|
-
const run = (args) => {
|
|
747
|
-
const remaining = deadline - Date.now();
|
|
748
|
-
if (remaining <= 0) throw new Error("agent-device cleanup timed out");
|
|
749
|
-
return exec.runFile("agent-device", [
|
|
750
|
-
...args,
|
|
751
|
-
"--json",
|
|
752
|
-
"--daemon-transport",
|
|
753
|
-
"socket"
|
|
754
|
-
], {
|
|
755
|
-
timeoutMs: Math.min(5e3, remaining),
|
|
756
|
-
killSignal: "SIGKILL"
|
|
757
|
-
});
|
|
758
|
-
};
|
|
759
|
-
const list = () => parseAgentDeviceSessions(run([
|
|
760
|
-
"session",
|
|
761
|
-
"list",
|
|
762
|
-
"--session",
|
|
763
|
-
"stim-teardown-inventory"
|
|
764
|
-
]));
|
|
765
|
-
try {
|
|
766
|
-
if (!exec.findExecutable("agent-device")) return;
|
|
767
|
-
const sessions = list().filter((session) => session.platform === device.platform && session.id === device.id);
|
|
768
|
-
for (const session of sessions) try {
|
|
769
|
-
if (!list().some((current) => current.name === session.name && current.platform === session.platform && current.id === session.id && current.createdAt === session.createdAt)) continue;
|
|
770
|
-
if (!stillOwned()) return;
|
|
771
|
-
if (JSON.parse(run([
|
|
772
|
-
"close",
|
|
773
|
-
"--session",
|
|
774
|
-
session.name,
|
|
775
|
-
"--session-lock",
|
|
776
|
-
"reject",
|
|
777
|
-
"--platform",
|
|
778
|
-
device.platform,
|
|
779
|
-
device.platform === "ios" ? "--udid" : "--serial",
|
|
780
|
-
device.id
|
|
781
|
-
]))?.success !== true) throw new Error("agent-device did not confirm session close");
|
|
782
|
-
report(`closed agent-device session ${session.name} on ${device.id}`);
|
|
783
|
-
} catch (error) {
|
|
784
|
-
report(`could not close agent-device session ${session.name} on ${device.id}: ${error.message}`);
|
|
785
|
-
}
|
|
786
|
-
} catch (error) {
|
|
787
|
-
report(`could not list agent-device sessions for ${device.id}: ${error.message}`);
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
//#endregion
|
|
791
279
|
//#region src/devices/teardown.ts
|
|
792
280
|
function teardownParkedAvd(avdName) {
|
|
793
281
|
try {
|
|
@@ -837,19 +325,33 @@ function teardownParkedIosSim(udid, { label, deleteSim = deleteParkedIosSim } =
|
|
|
837
325
|
};
|
|
838
326
|
}
|
|
839
327
|
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
328
|
+
const IOS_SHUTDOWN_SETTLE_MS = 15e3;
|
|
329
|
+
const IOS_SHUTDOWN_POLL_MS = 250;
|
|
330
|
+
const IOS_SHUTDOWN_ATTEMPTS = 2;
|
|
331
|
+
function settleIosSimShutdown(udid, { now = Date.now, sleep = sleepSync }) {
|
|
332
|
+
let resolved = resolveOwnedIosSim(udid);
|
|
333
|
+
for (let attempt = 1;; attempt++) {
|
|
334
|
+
const deadline = now() + IOS_SHUTDOWN_SETTLE_MS;
|
|
335
|
+
while (resolved.sim && resolved.sim.state !== "Shutdown" && now() < deadline) {
|
|
336
|
+
sleep(IOS_SHUTDOWN_POLL_MS);
|
|
337
|
+
resolved = resolveOwnedIosSim(udid);
|
|
338
|
+
}
|
|
339
|
+
if (!resolved.sim || resolved.sim.state === "Shutdown" || attempt === IOS_SHUTDOWN_ATTEMPTS) return resolved;
|
|
340
|
+
shutdownIosSim(udid);
|
|
341
|
+
resolved = resolveOwnedIosSim(udid);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function iosShutdownFailureReason(udid, state) {
|
|
345
|
+
return `simulator ${udid} is still ${state} after ${IOS_SHUTDOWN_ATTEMPTS} shutdown attempts and ${IOS_SHUTDOWN_ATTEMPTS * IOS_SHUTDOWN_SETTLE_MS / 1e3}s of waiting`;
|
|
346
|
+
}
|
|
347
|
+
function parkOwnedIosSim(settled, udid, park) {
|
|
348
|
+
if (settled.missing) throw new Error(`simulator ${udid} disappeared after shutdown`);
|
|
349
|
+
if (settled.notOwned) throw new Error(`simulator ${udid} is now named ${JSON.stringify(settled.notOwned)}`);
|
|
350
|
+
const sim = settled.sim;
|
|
351
|
+
if (sim.state !== "Shutdown") throw new Error(iosShutdownFailureReason(udid, sim.state));
|
|
846
352
|
const model = listIosDeviceTypes().find((d) => d.identifier === sim.deviceTypeIdentifier)?.name ?? null;
|
|
847
353
|
const runtime = parseRuntimeVersion(sim.runtime);
|
|
848
|
-
|
|
849
|
-
if (!sim.dataPath) throw new Error(`simulator ${udid} did not report a data path for app cleanup`);
|
|
850
|
-
const container = findAppDataContainer(sim.dataPath, park.bundleId);
|
|
851
|
-
if (container) clearAppDataContainer(container);
|
|
852
|
-
}
|
|
354
|
+
eraseIosSim(sim.udid);
|
|
853
355
|
const name = parkedSimName(sim.udid, {
|
|
854
356
|
model,
|
|
855
357
|
runtime
|
|
@@ -861,9 +363,7 @@ function parkOwnedIosSim(udid, park) {
|
|
|
861
363
|
deviceTypeIdentifier: sim.deviceTypeIdentifier,
|
|
862
364
|
runtimeIdentifier: sim.runtime,
|
|
863
365
|
parkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
864
|
-
simslimManaged: Boolean(park.simslimManaged)
|
|
865
|
-
...park.bundleId ? { bundleId: park.bundleId } : {},
|
|
866
|
-
...park.cacheKey ? { cacheKey: park.cacheKey } : {}
|
|
366
|
+
simslimManaged: Boolean(park.simslimManaged)
|
|
867
367
|
};
|
|
868
368
|
return {
|
|
869
369
|
record,
|
|
@@ -876,14 +376,14 @@ function parkOwnedIosSim(udid, park) {
|
|
|
876
376
|
})
|
|
877
377
|
};
|
|
878
378
|
}
|
|
879
|
-
function teardownOwnedIosSim(udid, { del = false, label, park } = {}) {
|
|
379
|
+
function teardownOwnedIosSim(udid, { del = false, label, park, shutdownClock = {} } = {}) {
|
|
880
380
|
let parkFallback;
|
|
881
381
|
try {
|
|
882
382
|
const resolved = resolveOwnedIosSim(udid);
|
|
883
383
|
if (resolved.notOwned) return {
|
|
884
384
|
status: "skipped",
|
|
885
385
|
kind: "not-owned",
|
|
886
|
-
reason: `sim
|
|
386
|
+
reason: `sim "${resolved.notOwned}" is not Stim-owned: Stim has no record of creating it`
|
|
887
387
|
};
|
|
888
388
|
if (resolved.missing) return { status: "missing" };
|
|
889
389
|
closeOwnedDeviceSessions({
|
|
@@ -900,7 +400,7 @@ function teardownOwnedIosSim(udid, { del = false, label, park } = {}) {
|
|
|
900
400
|
shutdownIosSim(udid);
|
|
901
401
|
const sim = resolved.sim;
|
|
902
402
|
if (del && park && park.max > 0) try {
|
|
903
|
-
const { record, evicted } = parkOwnedIosSim(udid, park);
|
|
403
|
+
const { record, evicted } = parkOwnedIosSim(settleIosSimShutdown(udid, shutdownClock), udid, park);
|
|
904
404
|
const removed = [];
|
|
905
405
|
const failures = [];
|
|
906
406
|
for (const entry of evicted) {
|
|
@@ -924,11 +424,23 @@ function teardownOwnedIosSim(udid, { del = false, label, park } = {}) {
|
|
|
924
424
|
} catch (e) {
|
|
925
425
|
parkFallback = String(e?.message || e);
|
|
926
426
|
}
|
|
927
|
-
if (del)
|
|
427
|
+
if (del) {
|
|
428
|
+
deleteIosSim(udid);
|
|
429
|
+
return {
|
|
430
|
+
status: "torn-down",
|
|
431
|
+
label: label ?? sim.name ?? udid,
|
|
432
|
+
...parkFallback ? { parkFallback } : {}
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
const settled = settleIosSimShutdown(udid, shutdownClock);
|
|
436
|
+
if (settled.sim && settled.sim.state !== "Shutdown") return {
|
|
437
|
+
status: "failed",
|
|
438
|
+
label: label ?? sim.name ?? udid,
|
|
439
|
+
reason: iosShutdownFailureReason(udid, settled.sim.state)
|
|
440
|
+
};
|
|
928
441
|
return {
|
|
929
442
|
status: "torn-down",
|
|
930
|
-
label: label ?? sim.name ?? udid
|
|
931
|
-
...parkFallback ? { parkFallback } : {}
|
|
443
|
+
label: label ?? sim.name ?? udid
|
|
932
444
|
};
|
|
933
445
|
} catch (e) {
|
|
934
446
|
return {
|
|
@@ -988,13 +500,13 @@ function teardownUnregisteredAvd(avdName, del, owner, surveyed) {
|
|
|
988
500
|
};
|
|
989
501
|
}
|
|
990
502
|
function teardownOwnedAvd(avdName, options = {}) {
|
|
991
|
-
if (!/^stim-[A-Za-z0-9._-]+$/.test(avdName)) return {
|
|
992
|
-
status: "skipped",
|
|
993
|
-
kind: "not-owned",
|
|
994
|
-
reason: `AVD ${avdName} is not Stim-owned by name`
|
|
995
|
-
};
|
|
996
503
|
let claim;
|
|
997
504
|
try {
|
|
505
|
+
if (!isStimOwnedAvdName(avdName, options.orphanedDirectory ? options.orphanedDirectory.directory : void 0)) return {
|
|
506
|
+
status: "skipped",
|
|
507
|
+
kind: "not-owned",
|
|
508
|
+
reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
|
|
509
|
+
};
|
|
998
510
|
claim = acquireAvdClaim(avdName);
|
|
999
511
|
const result = teardownClaimedAvd(avdName, claim, options);
|
|
1000
512
|
if (result.status === "torn-down" || result.status === "missing") options.onRemoved?.();
|
|
@@ -1016,7 +528,7 @@ function teardownClaimedAvd(avdName, claim, { del = false, park, owner, orphaned
|
|
|
1016
528
|
if (resolved.notOwned) return {
|
|
1017
529
|
status: "skipped",
|
|
1018
530
|
kind: "not-owned",
|
|
1019
|
-
reason: `AVD ${avdName} is not Stim-owned
|
|
531
|
+
reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
|
|
1020
532
|
};
|
|
1021
533
|
if (resolved.missing) return teardownUnregisteredAvd(avdName, del, owner, orphanedDirectory);
|
|
1022
534
|
if (onlyIfMissing) return {
|
|
@@ -1052,7 +564,7 @@ function teardownClaimedAvd(avdName, claim, { del = false, park, owner, orphaned
|
|
|
1052
564
|
if (current.notOwned) return {
|
|
1053
565
|
status: "skipped",
|
|
1054
566
|
kind: "not-owned",
|
|
1055
|
-
reason: `AVD ${avdName} is not Stim-owned
|
|
567
|
+
reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
|
|
1056
568
|
};
|
|
1057
569
|
if (current.missing) return teardownUnregisteredAvd(avdName, del, owner);
|
|
1058
570
|
if (current.serial) throw new Error(`Owned AVD ${avdName} started again before deletion.`);
|
|
@@ -1061,6 +573,9 @@ function teardownClaimedAvd(avdName, claim, { del = false, park, owner, orphaned
|
|
|
1061
573
|
if (park && park.max > 0) try {
|
|
1062
574
|
const systemImage = ownedAvdSystemImage(avdName);
|
|
1063
575
|
if (!systemImage || !park.configuration || !ownedAvdMatchesConfiguration(avdName, park.configuration)) throw new Error("the AVD has no verified creation configuration");
|
|
576
|
+
const directory = ownedAvdDirectory(avdName);
|
|
577
|
+
if (!directory) throw new Error("the AVD data directory could not be resolved");
|
|
578
|
+
wipeAvdUserData(directory);
|
|
1064
579
|
const evicted = parkSim({
|
|
1065
580
|
platform: "android",
|
|
1066
581
|
projectPath: park.projectPath,
|
|
@@ -1125,4 +640,541 @@ function teardownClaimedAvd(avdName, claim, { del = false, park, owner, orphaned
|
|
|
1125
640
|
}
|
|
1126
641
|
}
|
|
1127
642
|
//#endregion
|
|
1128
|
-
|
|
643
|
+
//#region src/engine/public-http-probe.ts
|
|
644
|
+
const DNS_TIMEOUT_MS = 5e3;
|
|
645
|
+
async function resolveAddresses(hostname, signal) {
|
|
646
|
+
if (signal.aborted) throw signal.reason;
|
|
647
|
+
let timer;
|
|
648
|
+
let onAbort;
|
|
649
|
+
const unavailable = new Promise((_resolve, reject) => {
|
|
650
|
+
onAbort = () => reject(signal.reason);
|
|
651
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
652
|
+
if (signal.aborted) onAbort();
|
|
653
|
+
else timer = setTimeout(() => reject(/* @__PURE__ */ new Error("DNS lookup timed out")), DNS_TIMEOUT_MS);
|
|
654
|
+
});
|
|
655
|
+
try {
|
|
656
|
+
return await Promise.race([resolve4(hostname), unavailable]);
|
|
657
|
+
} finally {
|
|
658
|
+
clearTimeout(timer);
|
|
659
|
+
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
async function probePublicHttp(url, signal) {
|
|
663
|
+
try {
|
|
664
|
+
return (await fetch(url, {
|
|
665
|
+
signal,
|
|
666
|
+
redirect: "follow"
|
|
667
|
+
})).status;
|
|
668
|
+
} catch (error) {
|
|
669
|
+
const cause = error?.cause;
|
|
670
|
+
if (cause?.code !== "ENOTFOUND" && cause?.code !== "EAI_AGAIN") return null;
|
|
671
|
+
}
|
|
672
|
+
const hostname = new URL(url).hostname;
|
|
673
|
+
if (!hostname.endsWith(".trycloudflare.com")) return null;
|
|
674
|
+
let addresses;
|
|
675
|
+
try {
|
|
676
|
+
addresses = await resolveAddresses(hostname, signal);
|
|
677
|
+
} catch {
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
const firstAddress = addresses[0];
|
|
681
|
+
if (!firstAddress) return null;
|
|
682
|
+
return new Promise((resolve) => {
|
|
683
|
+
const request$1 = request(url, {
|
|
684
|
+
signal,
|
|
685
|
+
lookup: (_hostname, options, callback) => {
|
|
686
|
+
if (options.all) callback(null, addresses.map((address) => ({
|
|
687
|
+
address,
|
|
688
|
+
family: 4
|
|
689
|
+
})));
|
|
690
|
+
else callback(null, firstAddress, 4);
|
|
691
|
+
}
|
|
692
|
+
}, (response) => {
|
|
693
|
+
response.resume();
|
|
694
|
+
resolve(response.statusCode ?? null);
|
|
695
|
+
});
|
|
696
|
+
request$1.on("error", () => resolve(null));
|
|
697
|
+
request$1.end();
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
//#endregion
|
|
701
|
+
//#region src/engine/tunnel.ts
|
|
702
|
+
function tunnelArgv(provider, port, ngrokUrl, logFile) {
|
|
703
|
+
if (provider === "cloudflared") return {
|
|
704
|
+
bin: "cloudflared",
|
|
705
|
+
args: [
|
|
706
|
+
"tunnel",
|
|
707
|
+
"--url",
|
|
708
|
+
`http://127.0.0.1:${port}`,
|
|
709
|
+
...logFile ? ["--logfile", logFile] : []
|
|
710
|
+
]
|
|
711
|
+
};
|
|
712
|
+
return {
|
|
713
|
+
bin: "ngrok",
|
|
714
|
+
args: [
|
|
715
|
+
"http",
|
|
716
|
+
String(port),
|
|
717
|
+
`--log=${logFile || "stdout"}`,
|
|
718
|
+
"--log-format=json",
|
|
719
|
+
...ngrokUrl ? ["--url", ngrokUrl] : []
|
|
720
|
+
]
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
const CLOUDFLARED_URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/i;
|
|
724
|
+
function parseCloudflaredLine(line) {
|
|
725
|
+
const match = line.match(CLOUDFLARED_URL_RE);
|
|
726
|
+
return match ? match[0] : null;
|
|
727
|
+
}
|
|
728
|
+
function parseNgrokLine(line) {
|
|
729
|
+
let data;
|
|
730
|
+
try {
|
|
731
|
+
data = JSON.parse(line);
|
|
732
|
+
} catch {
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
735
|
+
if (typeof data !== "object" || data === null) return null;
|
|
736
|
+
const url = data.url;
|
|
737
|
+
return typeof url === "string" && /^https?:\/\//i.test(url) ? url : null;
|
|
738
|
+
}
|
|
739
|
+
function parserFor(provider) {
|
|
740
|
+
return provider === "cloudflared" ? parseCloudflaredLine : parseNgrokLine;
|
|
741
|
+
}
|
|
742
|
+
const URL_TIMEOUT_MS = 15e3;
|
|
743
|
+
const REACHABLE_TIMEOUT_MS = 24e4;
|
|
744
|
+
const REACHABLE_POLL_MS = 2e3;
|
|
745
|
+
const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
746
|
+
async function withManagedTunnelLock(root, fn, options = {}) {
|
|
747
|
+
return withWorkspaceProcessLock(managedTunnelLockRoot(root), "metro-tunnel", fn, {
|
|
748
|
+
...options,
|
|
749
|
+
rejectOwnerPurposes: ["workspace removal"]
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
async function withManagedTunnelRemovalLock(root, fn, options = {}) {
|
|
753
|
+
return withWorkspaceProcessLock(managedTunnelLockRoot(root), "metro-tunnel", fn, {
|
|
754
|
+
...options,
|
|
755
|
+
ownerPurpose: "workspace removal"
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
async function withManagedRemoteWorktreeLock(worktreeRoot, fn, options = {}) {
|
|
759
|
+
return withWorkspaceProcessLock(managedRemoteWorktreeLockRoot(worktreeRoot), "managed-remote", fn, {
|
|
760
|
+
...options,
|
|
761
|
+
external: true,
|
|
762
|
+
rejectOwnerPurposes: ["worktree removal"]
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
async function withManagedRemoteWorktreeRemovalLock(worktreeRoot, fn, options = {}) {
|
|
766
|
+
return withWorkspaceProcessLock(managedRemoteWorktreeLockRoot(worktreeRoot), "managed-remote", fn, {
|
|
767
|
+
...options,
|
|
768
|
+
external: true,
|
|
769
|
+
ownerPurpose: "worktree removal"
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
function managedLockHolders(root) {
|
|
773
|
+
const locks = [{
|
|
774
|
+
label: "managed tunnel",
|
|
775
|
+
path: workspaceProcessLockPath(managedTunnelLockRoot(root), "metro-tunnel", false)
|
|
776
|
+
}, {
|
|
777
|
+
label: "managed remote",
|
|
778
|
+
path: workspaceProcessLockPath(managedRemoteWorktreeLockRoot(root), "managed-remote", true)
|
|
779
|
+
}];
|
|
780
|
+
const reasons = [];
|
|
781
|
+
for (const { label, path } of locks) {
|
|
782
|
+
const claims = (0, ownership_claim_exports.readClaimSet)(path);
|
|
783
|
+
const holder = claims.live[0];
|
|
784
|
+
if (holder) {
|
|
785
|
+
const purpose = typeof holder.details.purpose === "string" ? ` for ${holder.details.purpose}` : "";
|
|
786
|
+
reasons.push(`the ${label} lock is held${purpose}`);
|
|
787
|
+
} else if (claims.unresolved[0]) reasons.push(`the ${label} lock cannot be resolved: ${claims.unresolved[0].reason}`);
|
|
788
|
+
}
|
|
789
|
+
return reasons;
|
|
790
|
+
}
|
|
791
|
+
function managedTunnelLockRoot(root) {
|
|
792
|
+
const key = createHash("sha256").update(resolve(root)).digest("hex");
|
|
793
|
+
return join(getConfigDir(), "process-locks", key);
|
|
794
|
+
}
|
|
795
|
+
function managedRemoteWorktreeLockRoot(worktreeRoot) {
|
|
796
|
+
const key = createHash("sha256").update(resolve(worktreeRoot)).digest("hex");
|
|
797
|
+
return join(getConfigDir(), "process-locks", "worktrees", key);
|
|
798
|
+
}
|
|
799
|
+
async function defaultProbeReachable(url, signal) {
|
|
800
|
+
const status = await probePublicHttp(url, signal);
|
|
801
|
+
return status !== null && status > 0;
|
|
802
|
+
}
|
|
803
|
+
function waitForUrl(child, parseLine, timeoutMs) {
|
|
804
|
+
return new Promise((resolve) => {
|
|
805
|
+
let settled = false;
|
|
806
|
+
let timer;
|
|
807
|
+
let onOut;
|
|
808
|
+
let onErr;
|
|
809
|
+
let onError;
|
|
810
|
+
let onExit;
|
|
811
|
+
const finish = (url, exited = false) => {
|
|
812
|
+
if (settled) return;
|
|
813
|
+
settled = true;
|
|
814
|
+
clearTimeout(timer);
|
|
815
|
+
child.stdout?.removeListener("data", onOut);
|
|
816
|
+
child.stderr?.removeListener("data", onErr);
|
|
817
|
+
child.removeListener("error", onError);
|
|
818
|
+
child.removeListener("exit", onExit);
|
|
819
|
+
resolve({
|
|
820
|
+
url,
|
|
821
|
+
exited
|
|
822
|
+
});
|
|
823
|
+
};
|
|
824
|
+
const onLine = (line) => {
|
|
825
|
+
if (settled) return;
|
|
826
|
+
const url = parseLine(line);
|
|
827
|
+
if (url) finish(url);
|
|
828
|
+
};
|
|
829
|
+
const outReader = createLineReader(onLine);
|
|
830
|
+
const errReader = createLineReader(onLine);
|
|
831
|
+
onOut = (chunk) => outReader.push(chunk);
|
|
832
|
+
onErr = (chunk) => errReader.push(chunk);
|
|
833
|
+
onError = () => finish(null);
|
|
834
|
+
onExit = () => finish(null, true);
|
|
835
|
+
child.stdout?.setEncoding?.("utf-8");
|
|
836
|
+
child.stderr?.setEncoding?.("utf-8");
|
|
837
|
+
child.stdout?.on("data", onOut);
|
|
838
|
+
child.stderr?.on("data", onErr);
|
|
839
|
+
child.on("error", onError);
|
|
840
|
+
child.on("exit", onExit);
|
|
841
|
+
timer = setTimeout(() => finish(null), timeoutMs);
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
function waitForFileUrl(child, logFile, parseLine, timeoutMs) {
|
|
845
|
+
return new Promise((resolve) => {
|
|
846
|
+
let settled = false;
|
|
847
|
+
const finish = (url, exited = false) => {
|
|
848
|
+
if (settled) return;
|
|
849
|
+
settled = true;
|
|
850
|
+
clearInterval(interval);
|
|
851
|
+
clearTimeout(timer);
|
|
852
|
+
child.removeListener("error", onError);
|
|
853
|
+
child.removeListener("exit", onExit);
|
|
854
|
+
resolve({
|
|
855
|
+
url,
|
|
856
|
+
exited
|
|
857
|
+
});
|
|
858
|
+
};
|
|
859
|
+
const probe = () => {
|
|
860
|
+
try {
|
|
861
|
+
for (const line of readFileSync(logFile, "utf-8").split(/\r?\n/)) {
|
|
862
|
+
const url = parseLine(line);
|
|
863
|
+
if (url) return finish(url);
|
|
864
|
+
}
|
|
865
|
+
} catch {}
|
|
866
|
+
};
|
|
867
|
+
const onError = () => finish(null);
|
|
868
|
+
const onExit = () => finish(null, true);
|
|
869
|
+
child.on("error", onError);
|
|
870
|
+
child.on("exit", onExit);
|
|
871
|
+
const interval = setInterval(probe, 25);
|
|
872
|
+
const timer = setTimeout(() => finish(null), timeoutMs);
|
|
873
|
+
probe();
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
function createTunnelLogFile(provider) {
|
|
877
|
+
const dir = join(getConfigDir(), "tunnel-logs");
|
|
878
|
+
mkdirSync(dir, { recursive: true });
|
|
879
|
+
const path = join(dir, `${provider}-${randomUUID()}.log`);
|
|
880
|
+
closeSync(openSync(path, "wx", 384));
|
|
881
|
+
return path;
|
|
882
|
+
}
|
|
883
|
+
function removeTunnelLogFile(path) {
|
|
884
|
+
if (!path) return;
|
|
885
|
+
try {
|
|
886
|
+
unlinkSync(path);
|
|
887
|
+
} catch {}
|
|
888
|
+
}
|
|
889
|
+
function removeRecordedTunnelLogFile(path) {
|
|
890
|
+
if (!path) return;
|
|
891
|
+
const dir = resolve(join(getConfigDir(), "tunnel-logs"));
|
|
892
|
+
const resolved = resolve(path);
|
|
893
|
+
if (!resolved.startsWith(`${dir}${sep}`) || resolve(join(dir, basename(resolved))) !== resolved) return;
|
|
894
|
+
removeTunnelLogFile(resolved);
|
|
895
|
+
}
|
|
896
|
+
async function waitUntilReachable({ url, now, sleep, probe, timeoutMs }) {
|
|
897
|
+
const deadline = now() + timeoutMs;
|
|
898
|
+
for (;;) {
|
|
899
|
+
if (await probe(url, new AbortController().signal).catch(() => false)) return true;
|
|
900
|
+
if (now() >= deadline) return false;
|
|
901
|
+
await sleep(REACHABLE_POLL_MS);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
const CLEANUP_TIMEOUT_MS = 1e3;
|
|
905
|
+
const CLEANUP_POLL_MS = 25;
|
|
906
|
+
async function startTunnel({ provider, port, spawnFn = null, urlTimeoutMs = URL_TIMEOUT_MS, reachableTimeoutMs = REACHABLE_TIMEOUT_MS, probeReachable = defaultProbeReachable, now = Date.now, sleep = defaultSleep, ngrokUrl = null, requireReachable = true, cleanupTimeoutMs = CLEANUP_TIMEOUT_MS, isChildAlive = pidExists, readProcessToken = process_identity_exports.captureProcessToken, logFile = null }) {
|
|
907
|
+
const spawn = spawnFn || ((cmd, args, opts) => getExecutor().spawn(cmd, args, opts));
|
|
908
|
+
const outputFile = logFile || (spawnFn ? null : createTunnelLogFile(provider));
|
|
909
|
+
const { bin, args } = tunnelArgv(provider, port, ngrokUrl, outputFile);
|
|
910
|
+
let child;
|
|
911
|
+
try {
|
|
912
|
+
child = spawn(bin, args, {
|
|
913
|
+
stdio: outputFile ? [
|
|
914
|
+
"ignore",
|
|
915
|
+
"ignore",
|
|
916
|
+
"ignore"
|
|
917
|
+
] : [
|
|
918
|
+
"ignore",
|
|
919
|
+
"pipe",
|
|
920
|
+
"pipe"
|
|
921
|
+
],
|
|
922
|
+
detached: true
|
|
923
|
+
});
|
|
924
|
+
} catch (err) {
|
|
925
|
+
removeTunnelLogFile(outputFile);
|
|
926
|
+
return {
|
|
927
|
+
failed: true,
|
|
928
|
+
reason: `Could not start ${provider}: ${describe(err)}`
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
let childExited = false;
|
|
932
|
+
child.once("exit", () => {
|
|
933
|
+
childExited = true;
|
|
934
|
+
});
|
|
935
|
+
const cleanup = async (alreadyExited = childExited) => {
|
|
936
|
+
const stopped = await terminateChild(child, {
|
|
937
|
+
alreadyExited,
|
|
938
|
+
timeoutMs: cleanupTimeoutMs,
|
|
939
|
+
now,
|
|
940
|
+
sleep,
|
|
941
|
+
isAlive: isChildAlive
|
|
942
|
+
});
|
|
943
|
+
if (stopped) removeTunnelLogFile(outputFile);
|
|
944
|
+
return stopped ? { status: "stopped" } : {
|
|
945
|
+
status: "failed",
|
|
946
|
+
reason: `Sent SIGKILL but could not confirm that pid ${child.pid ?? "unknown"} exited.`
|
|
947
|
+
};
|
|
948
|
+
};
|
|
949
|
+
const failAfterCleanup = async (reason, alreadyExited = childExited) => {
|
|
950
|
+
const stopped = await cleanup(alreadyExited);
|
|
951
|
+
return stopped.status === "stopped" ? {
|
|
952
|
+
failed: true,
|
|
953
|
+
reason
|
|
954
|
+
} : {
|
|
955
|
+
failed: true,
|
|
956
|
+
reason: `${reason} ${stopped.reason}`,
|
|
957
|
+
cleanupFailed: true
|
|
958
|
+
};
|
|
959
|
+
};
|
|
960
|
+
const pid = child.pid;
|
|
961
|
+
if (!pid) return failAfterCleanup(`${provider} started but reported no pid.`);
|
|
962
|
+
const captureChildIdentity = () => {
|
|
963
|
+
try {
|
|
964
|
+
return readProcessToken(pid);
|
|
965
|
+
} catch {
|
|
966
|
+
return null;
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
const initialProcessToken = captureChildIdentity();
|
|
970
|
+
if (!initialProcessToken) return failAfterCleanup(`${provider} started but its process identity token could not be read.`);
|
|
971
|
+
const { url, exited } = outputFile ? await waitForFileUrl(child, outputFile, parserFor(provider), urlTimeoutMs) : await waitForUrl(child, parserFor(provider), urlTimeoutMs);
|
|
972
|
+
if (!url) return failAfterCleanup(exited ? `${provider} exited before printing a tunnel URL.` : `${provider} did not print a tunnel URL within ${urlTimeoutMs}ms.`, exited || childExited);
|
|
973
|
+
if (!outputFile) resumeChildPipes(child);
|
|
974
|
+
if (requireReachable) {
|
|
975
|
+
if (!await waitUntilReachable({
|
|
976
|
+
url,
|
|
977
|
+
now,
|
|
978
|
+
sleep,
|
|
979
|
+
probe: probeReachable,
|
|
980
|
+
timeoutMs: reachableTimeoutMs
|
|
981
|
+
})) return failAfterCleanup(`${url} did not become reachable within ${reachableTimeoutMs}ms; ${provider} may still be registering, or the network path is blocked.`);
|
|
982
|
+
}
|
|
983
|
+
const finalProcessToken = captureChildIdentity();
|
|
984
|
+
if (!finalProcessToken || finalProcessToken !== initialProcessToken) return failAfterCleanup(`${provider} process identity changed before its tunnel could be recorded.`);
|
|
985
|
+
if (!outputFile) unrefChildPipes(child);
|
|
986
|
+
child.unref?.();
|
|
987
|
+
if (childExited) return failAfterCleanup(`${provider} exited before its tunnel could be recorded.`, true);
|
|
988
|
+
return {
|
|
989
|
+
url,
|
|
990
|
+
pid,
|
|
991
|
+
processToken: initialProcessToken,
|
|
992
|
+
logFile: outputFile,
|
|
993
|
+
cleanup: () => cleanup()
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
async function startTunnelSequence({ providers, port, ngrokUrl = null, requireReachable = true, start = startTunnel }) {
|
|
997
|
+
const failures = [];
|
|
998
|
+
for (const provider of providers) {
|
|
999
|
+
const result = await start({
|
|
1000
|
+
provider,
|
|
1001
|
+
port,
|
|
1002
|
+
ngrokUrl: provider === "ngrok" ? ngrokUrl : null,
|
|
1003
|
+
requireReachable
|
|
1004
|
+
});
|
|
1005
|
+
if (!("failed" in result)) return {
|
|
1006
|
+
provider,
|
|
1007
|
+
...result
|
|
1008
|
+
};
|
|
1009
|
+
failures.push(`${provider}: ${result.reason}`);
|
|
1010
|
+
if (result.cleanupFailed) return {
|
|
1011
|
+
failed: true,
|
|
1012
|
+
reason: failures.join(" ")
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
return {
|
|
1016
|
+
failed: true,
|
|
1017
|
+
reason: failures.join(" ") || "No managed tunnel provider was selected."
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
function closeChildPipes(child) {
|
|
1021
|
+
child.stdout?.destroy?.();
|
|
1022
|
+
child.stderr?.destroy?.();
|
|
1023
|
+
}
|
|
1024
|
+
function unrefChildPipes(child) {
|
|
1025
|
+
resumeChildPipes(child);
|
|
1026
|
+
for (const stream of [child.stdout, child.stderr]) stream?.unref?.();
|
|
1027
|
+
}
|
|
1028
|
+
function resumeChildPipes(child) {
|
|
1029
|
+
child.stdout?.resume?.();
|
|
1030
|
+
child.stderr?.resume?.();
|
|
1031
|
+
}
|
|
1032
|
+
async function signalAndWaitForExit(child, signal, { timeoutMs, now, sleep, isAlive, platform }) {
|
|
1033
|
+
let exited = false;
|
|
1034
|
+
const onExit = () => {
|
|
1035
|
+
exited = true;
|
|
1036
|
+
};
|
|
1037
|
+
child.once("exit", onExit);
|
|
1038
|
+
try {
|
|
1039
|
+
if (platform === "win32" && child.pid) signalProcessTree(child.pid, signal, { platform });
|
|
1040
|
+
child.kill(signal);
|
|
1041
|
+
} catch (err) {
|
|
1042
|
+
child.removeListener("exit", onExit);
|
|
1043
|
+
return isEsrch(err);
|
|
1044
|
+
}
|
|
1045
|
+
const hasExited = () => exited;
|
|
1046
|
+
const deadline = now() + timeoutMs;
|
|
1047
|
+
while (!hasExited() && now() < deadline) await sleep(CLEANUP_POLL_MS);
|
|
1048
|
+
if (!hasExited() && child.pid && !isAlive(child.pid)) exited = true;
|
|
1049
|
+
child.removeListener("exit", onExit);
|
|
1050
|
+
return hasExited();
|
|
1051
|
+
}
|
|
1052
|
+
async function terminateChild(child, { alreadyExited, timeoutMs, now, sleep, isAlive, platform = process.platform }) {
|
|
1053
|
+
if (alreadyExited) {
|
|
1054
|
+
closeChildPipes(child);
|
|
1055
|
+
return true;
|
|
1056
|
+
}
|
|
1057
|
+
if (await signalAndWaitForExit(child, "SIGTERM", {
|
|
1058
|
+
timeoutMs,
|
|
1059
|
+
now,
|
|
1060
|
+
sleep,
|
|
1061
|
+
isAlive,
|
|
1062
|
+
platform
|
|
1063
|
+
})) {
|
|
1064
|
+
closeChildPipes(child);
|
|
1065
|
+
return true;
|
|
1066
|
+
}
|
|
1067
|
+
const killed = await signalAndWaitForExit(child, "SIGKILL", {
|
|
1068
|
+
timeoutMs,
|
|
1069
|
+
now,
|
|
1070
|
+
sleep,
|
|
1071
|
+
isAlive,
|
|
1072
|
+
platform
|
|
1073
|
+
});
|
|
1074
|
+
closeChildPipes(child);
|
|
1075
|
+
return killed;
|
|
1076
|
+
}
|
|
1077
|
+
function describe(err) {
|
|
1078
|
+
return err?.message || String(err);
|
|
1079
|
+
}
|
|
1080
|
+
const STOP_TIMEOUT_MS = 5e3;
|
|
1081
|
+
const STOP_POLL_MS = 100;
|
|
1082
|
+
function isEsrch(err) {
|
|
1083
|
+
return err?.code === "ESRCH";
|
|
1084
|
+
}
|
|
1085
|
+
async function stopTunnel(record, { isAlive = pidExists, inspectIdentity = process_identity_exports.inspectProcessIdentity, platform = process.platform, kill = (pid) => signalProcessTree(pid, "SIGTERM", { platform }), now = Date.now, sleep = defaultSleep, timeoutMs = STOP_TIMEOUT_MS } = {}) {
|
|
1086
|
+
const missing = () => {
|
|
1087
|
+
removeRecordedTunnelLogFile(record?.logFile);
|
|
1088
|
+
return { status: "missing" };
|
|
1089
|
+
};
|
|
1090
|
+
const pid = record?.pid;
|
|
1091
|
+
if (!pid || !isAlive(pid)) return missing();
|
|
1092
|
+
if (!record.processToken) return {
|
|
1093
|
+
status: "failed",
|
|
1094
|
+
reason: `tunnel pid ${pid} has no process identity token, so Stim cannot verify ownership. Keep the record. Inspect the process, stop it with the provider tooling, remove the stale metroTunnel state, and retry.`
|
|
1095
|
+
};
|
|
1096
|
+
const identity = inspectIdentity(record);
|
|
1097
|
+
if (identity === "gone" || identity === "different") return missing();
|
|
1098
|
+
if (identity !== "same") return {
|
|
1099
|
+
status: "failed",
|
|
1100
|
+
reason: `could not verify the process identity for tunnel pid ${pid}; refusing to signal it.`
|
|
1101
|
+
};
|
|
1102
|
+
try {
|
|
1103
|
+
kill(pid);
|
|
1104
|
+
} catch (err) {
|
|
1105
|
+
return isEsrch(err) ? missing() : {
|
|
1106
|
+
status: "failed",
|
|
1107
|
+
reason: describe(err)
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
const deadline = now() + timeoutMs;
|
|
1111
|
+
const exited = () => {
|
|
1112
|
+
if (!isAlive(pid)) return true;
|
|
1113
|
+
const current = inspectIdentity(record);
|
|
1114
|
+
return current === "gone" || current === "different";
|
|
1115
|
+
};
|
|
1116
|
+
while (now() < deadline && !exited()) await sleep(STOP_POLL_MS);
|
|
1117
|
+
if (!exited()) return {
|
|
1118
|
+
status: "failed",
|
|
1119
|
+
reason: `pid ${pid} did not exit within ${timeoutMs}ms.`
|
|
1120
|
+
};
|
|
1121
|
+
removeRecordedTunnelLogFile(record.logFile);
|
|
1122
|
+
return { status: "stopped" };
|
|
1123
|
+
}
|
|
1124
|
+
//#endregion
|
|
1125
|
+
//#region src/supervisor/ownership.ts
|
|
1126
|
+
function resolveSupervisorTarget({ state, record, reservedPort, isAlive = pidExists, inspectIdentity = process_identity_exports.inspectProcessIdentity } = {}) {
|
|
1127
|
+
const statePid = numberOrNull(state?.pid);
|
|
1128
|
+
const recordPid = numberOrNull(record?.pid);
|
|
1129
|
+
const pid = statePid ?? recordPid;
|
|
1130
|
+
if (!pid) return { status: "none" };
|
|
1131
|
+
if (statePid && recordPid && statePid !== recordPid) return {
|
|
1132
|
+
status: "unverified",
|
|
1133
|
+
pid,
|
|
1134
|
+
reason: `workspace state.json records supervisor pid ${statePid} but the registry records pid ${recordPid}`
|
|
1135
|
+
};
|
|
1136
|
+
if (!isAlive(pid)) return {
|
|
1137
|
+
status: "stale",
|
|
1138
|
+
pid
|
|
1139
|
+
};
|
|
1140
|
+
const port = numberOrNull(state?.port) ?? numberOrNull(record?.port);
|
|
1141
|
+
if (reservedPort !== null && reservedPort !== void 0 && port !== null && port !== reservedPort) return {
|
|
1142
|
+
status: "unverified",
|
|
1143
|
+
pid,
|
|
1144
|
+
reason: `supervisor pid ${pid} records port ${port}, but this project reserved port ${reservedPort}`
|
|
1145
|
+
};
|
|
1146
|
+
if (reservedPort !== null && reservedPort !== void 0 && port === null) return {
|
|
1147
|
+
status: "unverified",
|
|
1148
|
+
pid,
|
|
1149
|
+
reason: `supervisor pid ${pid} has no recorded port, so it cannot be matched against reserved port ${reservedPort}`
|
|
1150
|
+
};
|
|
1151
|
+
const identityRecord = statePid ? state : record;
|
|
1152
|
+
if (statePid && recordPid && state?.processToken !== record?.processToken) return {
|
|
1153
|
+
status: "unverified",
|
|
1154
|
+
pid,
|
|
1155
|
+
reason: "workspace state and registry process identities disagree"
|
|
1156
|
+
};
|
|
1157
|
+
const identity = inspectIdentity(identityRecord);
|
|
1158
|
+
if (identity === "gone" || identity === "different") return {
|
|
1159
|
+
status: "stale",
|
|
1160
|
+
pid
|
|
1161
|
+
};
|
|
1162
|
+
if (identity !== "same") return {
|
|
1163
|
+
status: "unverified",
|
|
1164
|
+
pid,
|
|
1165
|
+
reason: "the recorded process identity could not be verified"
|
|
1166
|
+
};
|
|
1167
|
+
return {
|
|
1168
|
+
status: "ours",
|
|
1169
|
+
processToken: identityRecord.processToken,
|
|
1170
|
+
pid,
|
|
1171
|
+
port,
|
|
1172
|
+
mode: state?.mode ?? record?.mode ?? null,
|
|
1173
|
+
startedAt: state?.startedAt ?? record?.startedAt ?? null
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
function numberOrNull(v) {
|
|
1177
|
+
return typeof v === "number" && Number.isSafeInteger(v) && v > 0 ? v : null;
|
|
1178
|
+
}
|
|
1179
|
+
//#endregion
|
|
1180
|
+
export { dropParked as _, terminateChild as a, readParked as b, withManagedTunnelLock as c, teardownOwnedAvd as d, teardownOwnedIosSim as f, adoptParked as g, POOL_SETTING_REMEDY as h, stopTunnel as i, withManagedTunnelRemovalLock as l, teardownParkedIosSim as m, managedLockHolders as n, withManagedRemoteWorktreeLock as o, teardownParkedAvd as p, startTunnelSequence as r, withManagedRemoteWorktreeRemovalLock as s, resolveSupervisorTarget as t, probePublicHttp as u, isLegacyDeletionClaim as v, selectParked as x, parkedMaxSetting as y };
|