stim 1.3.0 → 1.4.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 +17 -3
- package/dist/{android-CqcW1hrP.mjs → android-BTR1KTMP.mjs} +206 -120
- package/dist/{android-BqVIJoG7.mjs → android-BkM4qTaN.mjs} +76 -77
- package/dist/{android-cas-BR5YJBYv.mjs → android-cas-BjLRZyks.mjs} +15 -12
- package/dist/{app-install-DIp9riNx.mjs → app-install--PAocWvq.mjs} +2 -2
- package/dist/{build-slots-C3-R4XQr.mjs → build-slots-D6sDzOe-.mjs} +7 -8
- package/dist/{cache-manifest-CXR-Y8pH.mjs → cache-manifest-D02TpYjA.mjs} +1 -1
- package/dist/cache-manifest.mjs +1 -1
- package/dist/cli.mjs +14 -13
- package/dist/collector-run.mjs +4 -4
- package/dist/{command-output-8wHMEde0.mjs → command-output-BuIb_ZOI.mjs} +1 -4
- package/dist/{config-CcefMY2Q.mjs → config-6vdGngKa.mjs} +51 -51
- package/dist/dependency-state-BMirHEPD.mjs +76 -0
- package/dist/{deps-Spz8ozgo.mjs → deps-Ck4wuTwa.mjs} +1 -1
- package/dist/{dev-client-BjisHDB5.mjs → dev-client-Cd0v7iZk.mjs} +12 -12
- package/dist/{device-BkiW-gkk.mjs → device-CKXLnRRb.mjs} +7 -7
- package/dist/device-CMDFzx7C.mjs +1136 -0
- package/dist/{device-lease-BJYybF-2.mjs → device-lease-DDJr85OK.mjs} +3 -3
- package/dist/{device-pool-CPbLGfrB.mjs → device-pool-DJvbIBGW.mjs} +4 -4
- package/dist/{device-remote-AkYCKS8U.mjs → device-remote-CMfHhoc8.mjs} +7 -6
- package/dist/{doctor-Bmv86UUW.mjs → doctor-B_mfURdt.mjs} +536 -114
- package/dist/{error-diagnostics-DHC31rXA.mjs → error-diagnostics-_VmkqRZo.mjs} +8 -7
- package/dist/{gc-GGu-8ULN.mjs → gc-0AyxmECv.mjs} +61 -27
- package/dist/{guide-92CPA21s.mjs → guide-CGxdmQ0K.mjs} +236 -67
- package/dist/{ios-BSXFeO5H.mjs → ios-CtmGLx3X.mjs} +2667 -2607
- package/dist/{ios-device-CWOM_S44.mjs → ios-device-DJOi9FR5.mjs} +1 -1
- package/dist/{logs-DPNF4C9s.mjs → logs-BF3qPA7w.mjs} +32 -7
- package/dist/{logs-query-R-Cqc6Qv.mjs → logs-query-Cdp5aaCH.mjs} +1 -1
- package/dist/metro-DFLmaVz8.mjs +99 -0
- package/dist/named-ports-BO7Zpjp0.mjs +137 -0
- package/dist/{ownership-8O073GIC.mjs → ownership-BvSpZVBk.mjs} +5 -5
- package/dist/{ownership-BpCKiRlQ.mjs → ownership-C08Yzrp7.mjs} +6 -6
- package/dist/ports-BiKqP92J.mjs +39 -0
- package/dist/ports-ClqG8GPt.mjs +90 -0
- package/dist/{project-fKmuIaMI.mjs → project-CBFCwnN5.mjs} +2 -2
- package/dist/{reclaim-iW9Cad7k.mjs → reclaim-DpDVo-AP.mjs} +20 -13
- package/dist/{reload-DazHDSFA.mjs → reload-ByTqg6ZI.mjs} +8 -8
- package/dist/rolldown-runtime-BsOwBuO_.mjs +30 -0
- package/dist/{server-bare-DEzPZLvf.mjs → server-bare-Buk-k8tI.mjs} +3 -6
- package/dist/{server-expo-DizT4OMk.mjs → server-expo-D4P7iTvf.mjs} +3 -3
- package/dist/server-expo-Dho5Raiw.mjs +2 -0
- package/dist/{settings-DEJf6_kJ.mjs → settings-DwfltIXQ.mjs} +2 -2
- package/dist/{slot-launch-cci0xvmC.mjs → slot-launch-DeKen9e5.mjs} +4 -4
- package/dist/{start-mObjQH2W.mjs → start-BpKUHcU_.mjs} +16 -14
- package/dist/state-CDbvmZdt.mjs +131 -0
- package/dist/{stats-BR-kwj0B.mjs → stats-C59QLKm2.mjs} +1 -1
- package/dist/{stats-CaaQr4yN.mjs → stats-k5qMjyxo.mjs} +4 -4
- package/dist/{status-BxQNqu9t.mjs → status-Y2ij3dm6.mjs} +13 -12
- package/dist/{stop-DimGXEKY.mjs → stop-_P2OQwmZ.mjs} +15 -14
- package/dist/{stop-CrHGcjrV.mjs → stop-vKmmxhea.mjs} +2 -2
- package/dist/supervisor-run.d.mts +3 -27
- package/dist/supervisor-run.mjs +7 -6
- package/dist/{teardown-ClFp1Aax.mjs → teardown-KsPpvldq.mjs} +248 -110
- package/dist/{workspace-process-lock-3NcQnZ0X.mjs → workspace-process-lock-UEIrvZMJ.mjs} +4 -4
- package/dist/workspace-state-Do1e2mOH.mjs +146 -0
- package/dist/{worktree-DxTgN1CJ.mjs → worktree-BvcAIMdJ.mjs} +132 -123
- package/dist/{worktree-Eq6TLH8N.mjs → worktree-DWNx4tUg.mjs} +32 -2
- package/dist/xcode-D9tVItl-.mjs +860 -0
- package/package.json +7 -7
- package/dist/doctor-fAMXC4vl.mjs +0 -530
- package/dist/metro-CORqXyTL.mjs +0 -184
- package/dist/ownership-claim-Bf2CAfw3.mjs +0 -570
- package/dist/server-expo-44em5Pmg.mjs +0 -2
- package/dist/state-B3XA2wp2.mjs +0 -268
- package/dist/xcode-D7sAp-Zy.mjs +0 -1858
|
@@ -0,0 +1,1136 @@
|
|
|
1
|
+
import { t as getExecutor } from "./exec-CyylIdq9.mjs";
|
|
2
|
+
import { C as withConfigLock, E as ownership_claim_exports, F as deviceSlotPlatforms, L as projectDeviceSlots, T as acquireAvdClaim, _ as releaseAndroidConsolePort, b as setDevice, g as loadConfig, i as clearDevice, t as allConsolePortsAndSerials, u as getConfigDir, w as process_identity_exports, y as saveConfig } from "./config-6vdGngKa.mjs";
|
|
3
|
+
import { g as workspaceId } from "./workspace-state-Do1e2mOH.mjs";
|
|
4
|
+
import { s as ownedDeviceLabel } from "./project-CBFCwnN5.mjs";
|
|
5
|
+
import { n as stripAnsi, r as waitForChild, t as createLineReader } from "./process-output-C7IlIzd7.mjs";
|
|
6
|
+
import { r as pidExists } from "./metro-DFLmaVz8.mjs";
|
|
7
|
+
import { l as phaseLine } from "./command-output-BuIb_ZOI.mjs";
|
|
8
|
+
import { A as pickDefaultSystemImage, D as ownedAvdName, E as ownedAvdMatchesConfiguration, L as waitForBoot, N as resolveOwnedAvdSerial, O as ownedAvdSystemImage, b as listAvds, c as avdPoolConfiguration, d as configureNewOwnedAvd, f as createOwnedAvd, o as assertOwnedAvdStopped, u as bootAndroidEmulator, v as getAvdNameForSerial, w as nextConsolePort, x as listInstalledSystemImages, y as listAdbDevices } from "./android-BkM4qTaN.mjs";
|
|
9
|
+
import { l as androidDataPartitionSizeGbSetting, o as androidAvdConfigSetting, v as iosSimSlimProfileSetting } from "./settings-DwfltIXQ.mjs";
|
|
10
|
+
import { c as isLegacyDeletionClaim, d as selectParked, i as teardownParkedIosSim, l as parkedMaxSetting, o as adoptParked, r as teardownParkedAvd, s as dropParked, t as teardownOwnedAvd, u as readParked } from "./teardown-KsPpvldq.mjs";
|
|
11
|
+
import { t as withWorkspaceProcessLock } from "./workspace-process-lock-UEIrvZMJ.mjs";
|
|
12
|
+
import { E as readHostMemoryPressure, T as hostMemoryPressureAdvice, _ as renameIosSim, b as resolveIosCreation, c as iosRuntimeMatches, d as listIosDeviceTypes, g as parseRuntimeVersion, i as createOwnedIosSim, l as iosSimulatorFailureAdvice, m as ownedSimName, n as bootIosSim, u as listAllIosSims, v as resetIosKeychain, x as resolveOwnedIosSim, y as resetIosPrivacy } from "./ios-Deh5js5V.mjs";
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { claimRemoveCommand, clearClaimChild, markClaimChildPending, processGroupAlive, releaseClaim, setClaimChild } from "@stim-cli/core/ownership-claim";
|
|
16
|
+
import { captureProcessIdentity } from "@stim-cli/core/process-identity";
|
|
17
|
+
import { isDeepStrictEqual } from "node:util";
|
|
18
|
+
import chalk from "chalk";
|
|
19
|
+
//#region src/engine/simslim.ts
|
|
20
|
+
async function reconcileSimSlim({ udid, profile, previouslyManaged = false, out = () => {}, spawn, timeoutMs = 72e4, cleanupMs = 1e4 }) {
|
|
21
|
+
if (!profile && !previouslyManaged) return {
|
|
22
|
+
managed: false,
|
|
23
|
+
profile: null
|
|
24
|
+
};
|
|
25
|
+
const action = profile ? "on" : "off";
|
|
26
|
+
const args = profile ? [
|
|
27
|
+
"on",
|
|
28
|
+
udid,
|
|
29
|
+
"--profile",
|
|
30
|
+
profile
|
|
31
|
+
] : ["off", udid];
|
|
32
|
+
const lines = [];
|
|
33
|
+
const onLine = (raw) => {
|
|
34
|
+
const line = stripAnsi(raw).trim();
|
|
35
|
+
if (!line) return;
|
|
36
|
+
lines.push(line);
|
|
37
|
+
if (lines.length > 20) lines.shift();
|
|
38
|
+
out(`SimSlim: ${line}`);
|
|
39
|
+
};
|
|
40
|
+
const stdout = createLineReader(onLine);
|
|
41
|
+
const stderr = createLineReader(onLine);
|
|
42
|
+
const root = join(getConfigDir(), "simslim-locks", `${encodeURIComponent(udid.toLowerCase())}.lock`);
|
|
43
|
+
const label = `SimSlim for ${udid}`;
|
|
44
|
+
const attempt = (0, ownership_claim_exports.tryAcquireClaim)({
|
|
45
|
+
root,
|
|
46
|
+
mode: "exclusive",
|
|
47
|
+
label
|
|
48
|
+
});
|
|
49
|
+
if (attempt.pending) (0, ownership_claim_exports.releaseClaim)(attempt.pending);
|
|
50
|
+
const claim = attempt.acquired;
|
|
51
|
+
if (!claim) throw new ownership_claim_exports.ClaimRefusedError({
|
|
52
|
+
root,
|
|
53
|
+
claimPath: attempt.held?.path ?? root,
|
|
54
|
+
label,
|
|
55
|
+
reason: "another SimSlim operation is still using this simulator"
|
|
56
|
+
});
|
|
57
|
+
let child;
|
|
58
|
+
let result;
|
|
59
|
+
let identity;
|
|
60
|
+
let cancellation;
|
|
61
|
+
const stopGroup = () => {
|
|
62
|
+
if (child?.pid !== void 0 && identity && (0, process_identity_exports.inspectProcessIdentity)(identity) === "same") try {
|
|
63
|
+
process.kill(-child.pid, "SIGKILL");
|
|
64
|
+
} catch {}
|
|
65
|
+
};
|
|
66
|
+
const cancel = (signal) => {
|
|
67
|
+
cancellation ??= signal;
|
|
68
|
+
stopGroup();
|
|
69
|
+
};
|
|
70
|
+
const onInterrupt = () => cancel("SIGINT");
|
|
71
|
+
const onTerminate = () => cancel("SIGTERM");
|
|
72
|
+
const groupAlive = () => child?.pid !== void 0 && (0, ownership_claim_exports.processGroupAlive)(child.pid);
|
|
73
|
+
const settled = () => result !== void 0 && !groupAlive();
|
|
74
|
+
const waitUntil = async (deadline, cancellable = false) => {
|
|
75
|
+
while (!settled() && Date.now() < deadline) {
|
|
76
|
+
if (cancellable && cancellation) break;
|
|
77
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(50, deadline - Date.now())));
|
|
78
|
+
}
|
|
79
|
+
return settled();
|
|
80
|
+
};
|
|
81
|
+
process.on("SIGINT", onInterrupt);
|
|
82
|
+
process.on("SIGTERM", onTerminate);
|
|
83
|
+
process.on("exit", stopGroup);
|
|
84
|
+
try {
|
|
85
|
+
(0, ownership_claim_exports.markClaimChildPending)(claim);
|
|
86
|
+
try {
|
|
87
|
+
child = (spawn ?? ((cmd, childArgs, opts) => getExecutor().spawn(cmd, childArgs, opts)))("simslim", args, {
|
|
88
|
+
stdio: [
|
|
89
|
+
"ignore",
|
|
90
|
+
"pipe",
|
|
91
|
+
"pipe"
|
|
92
|
+
],
|
|
93
|
+
detached: true
|
|
94
|
+
});
|
|
95
|
+
} catch (error) {
|
|
96
|
+
throw simslimLaunchError(error);
|
|
97
|
+
}
|
|
98
|
+
child.stdout?.on("data", (chunk) => stdout.push(chunk));
|
|
99
|
+
child.stderr?.on("data", (chunk) => stderr.push(chunk));
|
|
100
|
+
waitForChild(child).then((value) => {
|
|
101
|
+
result = value;
|
|
102
|
+
});
|
|
103
|
+
if (child.pid !== void 0) {
|
|
104
|
+
const captured = (0, process_identity_exports.captureProcessIdentity)(child.pid);
|
|
105
|
+
if (captured.ok) {
|
|
106
|
+
identity = {
|
|
107
|
+
pid: child.pid,
|
|
108
|
+
processToken: captured.token
|
|
109
|
+
};
|
|
110
|
+
try {
|
|
111
|
+
(0, ownership_claim_exports.setClaimChild)(claim, identity);
|
|
112
|
+
} catch {}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const completed = await waitUntil(Date.now() + timeoutMs, true);
|
|
116
|
+
if (cancellation || !completed) {
|
|
117
|
+
stopGroup();
|
|
118
|
+
const stopped = await waitUntil(Date.now() + cleanupMs);
|
|
119
|
+
const detail = lines.length ? ` ${lines.join(" | ")}` : "";
|
|
120
|
+
const recovery = stopped ? "Its process group has stopped. Retry stim ios to reconcile the retained simulator settings." : `Its process group could not be confirmed stopped. The claim remains at ${claim.path}. Inspect the SimSlim process group ${child.pid ?? "unknown"} before retrying. Only when nothing is using it, remove the claim: ${(0, ownership_claim_exports.claimRemoveCommand)(claim.path)}`;
|
|
121
|
+
const message = `SimSlim ${action} ${cancellation ? `interrupted by ${cancellation}` : `exceeded ${Math.round(timeoutMs / 1e3)}s`}. ${recovery}${detail}`;
|
|
122
|
+
if (cancellation) out(message);
|
|
123
|
+
throw Object.assign(new Error(message), { code: cancellation ? "EINTR" : "ETIMEDOUT" });
|
|
124
|
+
}
|
|
125
|
+
} finally {
|
|
126
|
+
if (!child || settled()) (0, ownership_claim_exports.releaseClaim)(claim);
|
|
127
|
+
else {
|
|
128
|
+
child.stdout?.destroy?.();
|
|
129
|
+
child.stderr?.destroy?.();
|
|
130
|
+
child.unref();
|
|
131
|
+
}
|
|
132
|
+
process.off("SIGINT", onInterrupt);
|
|
133
|
+
process.off("SIGTERM", onTerminate);
|
|
134
|
+
process.off("exit", stopGroup);
|
|
135
|
+
if (cancellation) process.exit(cancellation === "SIGINT" ? 130 : 143);
|
|
136
|
+
}
|
|
137
|
+
stdout.flush();
|
|
138
|
+
stderr.flush();
|
|
139
|
+
if (result?.error) throw simslimLaunchError(result.error);
|
|
140
|
+
if (result?.code !== 0) {
|
|
141
|
+
const detail = lines.length ? ` ${lines.join(" | ")}` : "";
|
|
142
|
+
throw new Error(`SimSlim ${action} failed with exit code ${result?.code ?? "unknown"}.${detail}`);
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
managed: Boolean(profile),
|
|
146
|
+
profile: profile ?? null
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function simslimLaunchError(error) {
|
|
150
|
+
const cause = error;
|
|
151
|
+
if (cause?.code === "ENOENT") return new Error("SimSlim is configured but the `simslim` command is not installed. Run `brew install mobai-app/tap/simslim`.", { cause });
|
|
152
|
+
return new Error(`Could not start SimSlim: ${String(cause?.message || cause)}`, { cause });
|
|
153
|
+
}
|
|
154
|
+
function simslimIsOnPath() {
|
|
155
|
+
try {
|
|
156
|
+
return Boolean(getExecutor().runQuiet("command -v simslim", { timeoutMs: 5e3 }));
|
|
157
|
+
} catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
//#endregion
|
|
162
|
+
//#region src/engine/android-avd-setup.ts
|
|
163
|
+
var AvdBootError = class extends Error {
|
|
164
|
+
remedy;
|
|
165
|
+
constructor(message, remedy, cause) {
|
|
166
|
+
super(message, { cause });
|
|
167
|
+
this.remedy = remedy;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
var AvdRecoveryError = class extends AvdBootError {};
|
|
171
|
+
function findOtherOwner(avdName, projectPath, selectedSlot = "default") {
|
|
172
|
+
for (const [path, project] of Object.entries(loadConfig()?.projects ?? {})) if (projectDeviceSlots(project).some(({ slot, platforms }) => (path !== projectPath || slot !== selectedSlot) && platforms.android?.avdName?.toLowerCase() === avdName.toLowerCase())) return path;
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
async function prepareOwnedAvd({ projectPath, slot, label, previousAvdName, systemImage, configuration, configure, teardown = teardownOwnedAvd }) {
|
|
176
|
+
const { claim, reservation, previous, avdName } = withConfigLock(() => {
|
|
177
|
+
const previous = deviceSlotPlatforms(loadConfig()?.projects?.[projectPath], slot)?.android;
|
|
178
|
+
if (previous?.avdName && previous.avdName !== previousAvdName) throw new Error(`Another Stim run assigned AVD ${previous.avdName} to this workspace. Retry to use it.`);
|
|
179
|
+
if (readParked("android").some((entry) => entry.name === ownedAvdName(label)) || findOtherOwner(ownedAvdName(label), projectPath, slot)) label = `${label}-${randomUUID().slice(0, 8)}`;
|
|
180
|
+
const avdName = ownedAvdName(label);
|
|
181
|
+
const claim = acquireAvdClaim(avdName);
|
|
182
|
+
const reservation = {
|
|
183
|
+
avdName,
|
|
184
|
+
owned: true,
|
|
185
|
+
deviceName: avdName,
|
|
186
|
+
setupIncomplete: true,
|
|
187
|
+
poolConfiguration: configuration
|
|
188
|
+
};
|
|
189
|
+
try {
|
|
190
|
+
setDevice(projectPath, "android", reservation, slot);
|
|
191
|
+
return {
|
|
192
|
+
claim,
|
|
193
|
+
reservation,
|
|
194
|
+
previous,
|
|
195
|
+
avdName
|
|
196
|
+
};
|
|
197
|
+
} catch (error) {
|
|
198
|
+
releaseClaim(claim);
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
const currentRecord = () => deviceSlotPlatforms(loadConfig()?.projects?.[projectPath], slot)?.android;
|
|
203
|
+
const assertReservation = () => {
|
|
204
|
+
if (readParked("android").some((entry) => entry.name === avdName)) throw new Error(`AVD ${avdName} was parked by another Stim run. Retry to adopt it safely.`);
|
|
205
|
+
const owner = findOtherOwner(avdName, projectPath, slot);
|
|
206
|
+
if (owner) throw new Error(`AVD ${avdName} already exists and is owned by another project (${owner}). Retry to allocate a distinct owned emulator.`);
|
|
207
|
+
const current = currentRecord();
|
|
208
|
+
if (!isDeepStrictEqual(current, reservation)) {
|
|
209
|
+
const state = current?.setupIncomplete ? "has incomplete setup" : "was registered or removed";
|
|
210
|
+
throw new Error(`AVD ${current?.avdName ?? avdName} ${state} by another concurrent Stim run. Retry after that run finishes so the recorded device is resolved safely.`);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
let prepared;
|
|
214
|
+
let child;
|
|
215
|
+
const creationRunning = () => child?.pid !== void 0 && processGroupAlive(child.pid);
|
|
216
|
+
let configurationFailure;
|
|
217
|
+
try {
|
|
218
|
+
try {
|
|
219
|
+
const creation = await createOwnedAvd(label, {
|
|
220
|
+
systemImage,
|
|
221
|
+
spawn: (...args) => {
|
|
222
|
+
markClaimChildPending(claim);
|
|
223
|
+
child = getExecutor().spawn(...args);
|
|
224
|
+
if (child.pid !== void 0) {
|
|
225
|
+
const captured = captureProcessIdentity(child.pid);
|
|
226
|
+
if (captured.ok) try {
|
|
227
|
+
setClaimChild(claim, {
|
|
228
|
+
pid: child.pid,
|
|
229
|
+
processToken: captured.token
|
|
230
|
+
});
|
|
231
|
+
} catch {}
|
|
232
|
+
}
|
|
233
|
+
return child;
|
|
234
|
+
}
|
|
235
|
+
}).then((result) => ({ result }), (error) => ({ error }));
|
|
236
|
+
if (creationRunning()) throw new AvdRecoveryError(`AVD ${avdName} creation left its process group running (pid ${child.pid}); its reservation remains incomplete.`, `Wait for process group ${child.pid} to finish before retrying stim android. If its identity could not be recorded, inspect the group and remove the claim only after it stops: ${claimRemoveCommand(claim.path)}`);
|
|
237
|
+
clearClaimChild(claim);
|
|
238
|
+
if ("error" in creation) throw creation.error;
|
|
239
|
+
prepared = {
|
|
240
|
+
...creation.result,
|
|
241
|
+
created: true
|
|
242
|
+
};
|
|
243
|
+
} catch (error) {
|
|
244
|
+
const message = String(error?.message || error);
|
|
245
|
+
if (!message.includes("already exists")) throw error;
|
|
246
|
+
try {
|
|
247
|
+
if (!listAvds().includes(avdName)) throw new AvdRecoveryError(`AVD ${avdName} already exists on disk but is not listed by the emulator. ${message}`, "Run `npx stim gc` to inspect orphaned owned AVDs, then `npx stim gc --delete` to reclaim those safe to delete. Retry `stim android` after cleanup; keep any AVD that GC cannot verify.", error);
|
|
248
|
+
withConfigLock(assertReservation);
|
|
249
|
+
const resolved = resolveOwnedAvdSerial(avdName);
|
|
250
|
+
if (resolved.missing || resolved.notOwned) throw new Error(`AVD ${avdName} could not be verified for recovery. Retry after checking its registration.`, { cause: error });
|
|
251
|
+
if (!resolved.serial) assertOwnedAvdStopped(avdName);
|
|
252
|
+
prepared = {
|
|
253
|
+
avdName,
|
|
254
|
+
created: false,
|
|
255
|
+
systemImage: ownedAvdSystemImage(avdName),
|
|
256
|
+
...resolved.serial ? {
|
|
257
|
+
serial: resolved.serial,
|
|
258
|
+
consolePort: Number(resolved.serial.replace(/^emulator-/, ""))
|
|
259
|
+
} : {}
|
|
260
|
+
};
|
|
261
|
+
} catch (recoveryError) {
|
|
262
|
+
withConfigLock(() => {
|
|
263
|
+
if (!isDeepStrictEqual(currentRecord(), reservation)) return;
|
|
264
|
+
if (previous) setDevice(projectPath, "android", previous, slot);
|
|
265
|
+
else clearDevice(projectPath, "android", slot, avdName);
|
|
266
|
+
});
|
|
267
|
+
if (recoveryError instanceof AvdRecoveryError) throw recoveryError;
|
|
268
|
+
throw new AvdRecoveryError(`Could not recover owned AVD ${avdName}: ${String(recoveryError?.message || recoveryError)}`, "Inspect `npx stim status` and `adb devices`. Wait for any other Stim run using this AVD to finish, then retry `stim android`. Keep the AVD and its process locks while its state is unverified.", recoveryError);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (prepared.created) try {
|
|
272
|
+
configure(avdName);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
configurationFailure = { error };
|
|
275
|
+
}
|
|
276
|
+
if (!configurationFailure) withConfigLock(() => {
|
|
277
|
+
assertReservation();
|
|
278
|
+
setDevice(projectPath, "android", {
|
|
279
|
+
avdName,
|
|
280
|
+
owned: true,
|
|
281
|
+
deviceName: avdName,
|
|
282
|
+
...prepared.created ? { poolConfiguration: configuration } : {},
|
|
283
|
+
...prepared.consolePort ? { consolePort: prepared.consolePort } : {}
|
|
284
|
+
}, slot);
|
|
285
|
+
});
|
|
286
|
+
} finally {
|
|
287
|
+
if (!creationRunning()) releaseClaim(claim);
|
|
288
|
+
else {
|
|
289
|
+
child?.stdout?.destroy();
|
|
290
|
+
child?.stderr?.destroy();
|
|
291
|
+
child?.unref();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (configurationFailure) {
|
|
295
|
+
const { error } = configurationFailure;
|
|
296
|
+
const cleanup = teardown(avdName, {
|
|
297
|
+
del: true,
|
|
298
|
+
owner: {
|
|
299
|
+
projectPath,
|
|
300
|
+
slot,
|
|
301
|
+
expectedRecord: reservation
|
|
302
|
+
},
|
|
303
|
+
onRemoved: () => {
|
|
304
|
+
clearDevice(projectPath, "android", slot, avdName);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
const orphan = cleanup.status === "failed" || cleanup.status === "skipped" ? ` The owned AVD remains tracked for cleanup (${cleanup.reason || cleanup.status}); fix the cause, then retry or run \`stim gc --delete\`.` : "";
|
|
308
|
+
throw new Error(`Created owned AVD ${avdName}, but could not configure its AVD settings: ${String(error?.message || error)}${orphan}`, { cause: error });
|
|
309
|
+
}
|
|
310
|
+
return prepared;
|
|
311
|
+
}
|
|
312
|
+
//#endregion
|
|
313
|
+
//#region src/engine/device.ts
|
|
314
|
+
function startIosBoot(udid, configure, label, out) {
|
|
315
|
+
const done = (async () => {
|
|
316
|
+
await bootIosSim(udid, {
|
|
317
|
+
label,
|
|
318
|
+
out
|
|
319
|
+
});
|
|
320
|
+
await configure();
|
|
321
|
+
})();
|
|
322
|
+
done.catch(() => {});
|
|
323
|
+
return {
|
|
324
|
+
udid,
|
|
325
|
+
done
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
async function ensureOwnedDevice({ platform, project, projectPath, slot, settingsRoot = projectPath, label = ownedDeviceLabel(projectPath), settings, flags = {}, note = () => {}, out = () => {}, logFile = null, alive = pidExists, configureAvd = configureNewOwnedAvd, teardownAvd = teardownOwnedAvd, reconcileIosSimulator = reconcileSimSlim }) {
|
|
329
|
+
if (slot && slot !== "default") label = `${label}-${slot}`;
|
|
330
|
+
const record = deviceSlotPlatforms(project, slot)?.[platform] ?? null;
|
|
331
|
+
if (platform === "ios") return ensureOwnedIosDevice({
|
|
332
|
+
record,
|
|
333
|
+
projectPath,
|
|
334
|
+
slot,
|
|
335
|
+
settingsRoot,
|
|
336
|
+
label,
|
|
337
|
+
settings,
|
|
338
|
+
flags,
|
|
339
|
+
note,
|
|
340
|
+
out,
|
|
341
|
+
reconcileIosSimulator
|
|
342
|
+
});
|
|
343
|
+
return ensureOwnedAndroidDevice({
|
|
344
|
+
record,
|
|
345
|
+
projectPath,
|
|
346
|
+
slot,
|
|
347
|
+
settingsRoot,
|
|
348
|
+
label,
|
|
349
|
+
settings,
|
|
350
|
+
flags,
|
|
351
|
+
note,
|
|
352
|
+
out,
|
|
353
|
+
logFile,
|
|
354
|
+
alive,
|
|
355
|
+
configureAvd,
|
|
356
|
+
teardownAvd
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
async function ensureOwnedIosDevice({ record, projectPath, slot, settingsRoot, label, settings, flags, note, out, reconcileIosSimulator }) {
|
|
360
|
+
const memoryAdvice = hostMemoryPressureAdvice(readHostMemoryPressure());
|
|
361
|
+
if (memoryAdvice) out(chalk.yellow(phaseLine("memory", memoryAdvice)));
|
|
362
|
+
const simslimProfile = iosSimSlimProfileSetting(settings, settingsRoot);
|
|
363
|
+
if (record?.deviceUdid) {
|
|
364
|
+
if (record.owned) {
|
|
365
|
+
const resolved = resolveOwnedIosSim(record.deviceUdid);
|
|
366
|
+
if (resolved.notOwned) note(chalk.yellow(`Note: recorded sim is now named "${resolved.notOwned}", not Stim-owned by name -- creating a fresh owned sim instead of booting it.`));
|
|
367
|
+
else if (resolved.missing) {} else {
|
|
368
|
+
const sim = resolved.sim;
|
|
369
|
+
const wantedType = flags.deviceType || settings?.ios?.deviceType;
|
|
370
|
+
const deviceTypes = listIosDeviceTypes();
|
|
371
|
+
const mismatch = deviceTypeMismatch(sim.deviceTypeIdentifier, wantedType, deviceTypes);
|
|
372
|
+
if (mismatch) throw new Error(`${mismatch}. Stim will not silently boot a different model. Run \`stim worktree remove\` (or \`stim gc --delete\`) to reap the current sim, then \`stim ios\` again to create the requested one.`);
|
|
373
|
+
const model = {
|
|
374
|
+
model: deviceTypes.find((d) => d.identifier === sim.deviceTypeIdentifier)?.name ?? null,
|
|
375
|
+
runtime: parseRuntimeVersion(sim.runtime)
|
|
376
|
+
};
|
|
377
|
+
const name = await withIosDeviceNameLock(() => renameToOwnedName(sim, label, model, projectPath));
|
|
378
|
+
const updated = {
|
|
379
|
+
deviceUdid: sim.udid,
|
|
380
|
+
owned: true,
|
|
381
|
+
deviceName: name,
|
|
382
|
+
...record.simslimManaged ? { simslimManaged: true } : {},
|
|
383
|
+
...record.adopted ? { adopted: true } : {},
|
|
384
|
+
...record.adoptionPending ? { adoptionPending: true } : {},
|
|
385
|
+
...record.parkedCacheKey ? { parkedCacheKey: record.parkedCacheKey } : {}
|
|
386
|
+
};
|
|
387
|
+
const configure = async () => {
|
|
388
|
+
if (record.adoptionPending) resetAdoptedSim(sim.udid, out);
|
|
389
|
+
return configureOwnedIosSim({
|
|
390
|
+
record: updated,
|
|
391
|
+
projectPath,
|
|
392
|
+
slot,
|
|
393
|
+
profile: simslimProfile,
|
|
394
|
+
out,
|
|
395
|
+
reconcileIosSimulator
|
|
396
|
+
});
|
|
397
|
+
};
|
|
398
|
+
const facts = {
|
|
399
|
+
deviceType: model.model,
|
|
400
|
+
runtime: model.runtime
|
|
401
|
+
};
|
|
402
|
+
if (sim.state !== "Booted") {
|
|
403
|
+
out(chalk.dim(phaseLine("device", `booting ${name} (${sim.udid})`)));
|
|
404
|
+
return {
|
|
405
|
+
...updated,
|
|
406
|
+
booting: startIosBoot(sim.udid, configure, name, out),
|
|
407
|
+
...facts
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
return {
|
|
411
|
+
...await configure(),
|
|
412
|
+
...facts
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
} else {
|
|
416
|
+
const sim = listAllIosSims().find((s) => s.udid === record.deviceUdid);
|
|
417
|
+
if (sim) {
|
|
418
|
+
if (sim.state !== "Booted") {
|
|
419
|
+
note(chalk.yellow(`Note: assigned sim ${sim.name} (${sim.udid}) is shut down and is not owned by Stim, so it will not be booted automatically.`));
|
|
420
|
+
note(chalk.dim("Boot it yourself, or run `stim gc --delete` to clear the assignment so Stim can create an owned sim."));
|
|
421
|
+
}
|
|
422
|
+
return record;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
withConfigLock(() => {
|
|
426
|
+
const current = deviceSlotPlatforms(loadConfig()?.projects?.[projectPath], slot)?.ios;
|
|
427
|
+
if (!current) return;
|
|
428
|
+
if (current.deviceUdid !== record.deviceUdid || current.owned !== record.owned) throw new Error("The simulator assignment changed during recovery. Run `stim ios` again.");
|
|
429
|
+
clearDevice(projectPath, "ios", slot);
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
const choice = resolveIosCreation({
|
|
433
|
+
deviceType: flags.deviceType || settings.ios?.deviceType,
|
|
434
|
+
runtime: flags.runtime || settings.ios?.runtime
|
|
435
|
+
});
|
|
436
|
+
const adopted = parkedMaxSetting("ios").max > 0 ? await withIosDeviceNameLock(() => takeParkedIosSim({
|
|
437
|
+
projectPath,
|
|
438
|
+
slot,
|
|
439
|
+
label,
|
|
440
|
+
choice,
|
|
441
|
+
out
|
|
442
|
+
})) : null;
|
|
443
|
+
if (adopted) {
|
|
444
|
+
out(chalk.dim(phaseLine("device", `booting ${adopted.deviceName} (${adopted.deviceUdid})`)));
|
|
445
|
+
const booting = startIosBoot(adopted.deviceUdid, async () => {
|
|
446
|
+
resetAdoptedSim(adopted.deviceUdid, out);
|
|
447
|
+
await configureOwnedIosSim({
|
|
448
|
+
record: adopted,
|
|
449
|
+
projectPath,
|
|
450
|
+
slot,
|
|
451
|
+
profile: simslimProfile,
|
|
452
|
+
out,
|
|
453
|
+
reconcileIosSimulator
|
|
454
|
+
});
|
|
455
|
+
}, adopted.deviceName, out);
|
|
456
|
+
return {
|
|
457
|
+
...adopted,
|
|
458
|
+
booting,
|
|
459
|
+
deviceType: choice.deviceType,
|
|
460
|
+
runtime: choice.runtime
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
const created = await withIosDeviceNameLock(() => {
|
|
464
|
+
const suffix = ownedIosNameSuffix(label, {
|
|
465
|
+
model: choice.deviceType,
|
|
466
|
+
runtime: choice.runtime
|
|
467
|
+
}, projectPath);
|
|
468
|
+
const result = createOwnedIosSim(label, { suffix }, choice);
|
|
469
|
+
setDevice(projectPath, "ios", {
|
|
470
|
+
deviceUdid: result.udid,
|
|
471
|
+
owned: true,
|
|
472
|
+
deviceName: result.name
|
|
473
|
+
}, slot);
|
|
474
|
+
return result;
|
|
475
|
+
});
|
|
476
|
+
const newRecord = {
|
|
477
|
+
deviceUdid: created.udid,
|
|
478
|
+
owned: true,
|
|
479
|
+
deviceName: created.name
|
|
480
|
+
};
|
|
481
|
+
const booting = startIosBoot(created.udid, () => configureOwnedIosSim({
|
|
482
|
+
record: newRecord,
|
|
483
|
+
projectPath,
|
|
484
|
+
slot,
|
|
485
|
+
profile: simslimProfile,
|
|
486
|
+
out,
|
|
487
|
+
reconcileIosSimulator
|
|
488
|
+
}), created.name, out);
|
|
489
|
+
return {
|
|
490
|
+
...newRecord,
|
|
491
|
+
created: true,
|
|
492
|
+
booting,
|
|
493
|
+
deviceType: created.deviceType,
|
|
494
|
+
runtime: created.runtime
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
function withIosDeviceNameLock(fn) {
|
|
498
|
+
return withWorkspaceProcessLock(getConfigDir(), "ios-device-names", async () => fn(), { external: true });
|
|
499
|
+
}
|
|
500
|
+
function ownedIosNameSuffix(label, model, projectPath, udid) {
|
|
501
|
+
const names = new Set(listAllIosSims({ includeUnavailable: true }).filter((sim) => sim.udid !== udid).map((sim) => sim.name));
|
|
502
|
+
for (const project of Object.values(loadConfig()?.projects ?? {})) for (const { platforms } of projectDeviceSlots(project)) {
|
|
503
|
+
const record = platforms.ios;
|
|
504
|
+
if (record?.deviceUdid !== udid && record?.deviceName) names.add(record.deviceName);
|
|
505
|
+
}
|
|
506
|
+
for (const parked of readParked("ios")) if (parked.udid !== udid) names.add(parked.name);
|
|
507
|
+
let suffix = "";
|
|
508
|
+
for (let attempt = 0; names.has(ownedSimName(label, model, suffix)); attempt++) suffix = ` ${workspaceId(projectPath)}${attempt ? `-${attempt}` : ""}`;
|
|
509
|
+
return suffix;
|
|
510
|
+
}
|
|
511
|
+
function renameToOwnedName(sim, label, model, projectPath) {
|
|
512
|
+
const wanted = ownedSimName(label, model, ownedIosNameSuffix(label, model, projectPath, sim.udid));
|
|
513
|
+
if (sim.name === wanted) return wanted;
|
|
514
|
+
try {
|
|
515
|
+
renameIosSim(sim.udid, wanted);
|
|
516
|
+
return wanted;
|
|
517
|
+
} catch {
|
|
518
|
+
return sim.name;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function resetAdoptedSim(udid, out) {
|
|
522
|
+
out(chalk.dim(phaseLine("device", `resetting privacy grants and the keychain on ${udid}`)));
|
|
523
|
+
resetIosPrivacy(udid);
|
|
524
|
+
resetIosKeychain(udid);
|
|
525
|
+
}
|
|
526
|
+
function takeParkedIosSim({ projectPath, slot, label, choice, out }) {
|
|
527
|
+
const candidates = selectParked(readParked("ios"), {
|
|
528
|
+
deviceTypeIdentifier: choice.deviceTypeId,
|
|
529
|
+
runtimeIdentifier: choice.runtimeId
|
|
530
|
+
});
|
|
531
|
+
if (candidates.length === 0) return null;
|
|
532
|
+
const listed = new Map(listAllIosSims({ includeUnavailable: true }).map((sim) => [sim.udid, sim]));
|
|
533
|
+
for (const parked of candidates) {
|
|
534
|
+
const sim = listed.get(parked.udid);
|
|
535
|
+
if (!sim || !sim.available) {
|
|
536
|
+
const result = sim ? teardownParkedIosSim(parked.udid, { label: parked.name }) : null;
|
|
537
|
+
if (sim ? result?.status === "torn-down" : dropParked("ios", parked.udid)) out(chalk.dim(phaseLine("device", `deleted parked ${parked.name} (${parked.udid}): ${sim ? "unavailable" : "gone"}`)));
|
|
538
|
+
else if (result?.status === "failed") out(chalk.yellow(phaseLine("device", `could not delete unavailable parked ${parked.name} (${parked.udid}): ${result.reason}`)));
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
if (!sim.name.startsWith("stim-")) {
|
|
542
|
+
out(chalk.yellow(phaseLine("device", `kept parked record for ${parked.udid}: simulator is now named ${JSON.stringify(sim.name)} and is not Stim-owned`)));
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
const model = {
|
|
546
|
+
model: choice.deviceType,
|
|
547
|
+
runtime: choice.runtime
|
|
548
|
+
};
|
|
549
|
+
const name = ownedSimName(label, model, ownedIosNameSuffix(label, model, projectPath, parked.udid));
|
|
550
|
+
const device = {
|
|
551
|
+
deviceUdid: parked.udid,
|
|
552
|
+
owned: true,
|
|
553
|
+
deviceName: name,
|
|
554
|
+
adopted: true,
|
|
555
|
+
adoptionPending: true,
|
|
556
|
+
...parked.simslimManaged ? { simslimManaged: true } : {},
|
|
557
|
+
...parked.cacheKey ? { parkedCacheKey: parked.cacheKey } : {}
|
|
558
|
+
};
|
|
559
|
+
if (!adoptParked({
|
|
560
|
+
platform: "ios",
|
|
561
|
+
projectPath,
|
|
562
|
+
slot,
|
|
563
|
+
udid: parked.udid,
|
|
564
|
+
device
|
|
565
|
+
})) continue;
|
|
566
|
+
try {
|
|
567
|
+
renameIosSim(parked.udid, name);
|
|
568
|
+
} catch {}
|
|
569
|
+
return device;
|
|
570
|
+
}
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
function clearIosAdoptionPending(projectPath, slot = "default") {
|
|
574
|
+
withConfigLock(() => {
|
|
575
|
+
const cfg = loadConfig();
|
|
576
|
+
const ios = deviceSlotPlatforms(cfg?.projects?.[projectPath], slot)?.ios;
|
|
577
|
+
if (!cfg || !ios?.adoptionPending) return;
|
|
578
|
+
delete ios.adopted;
|
|
579
|
+
delete ios.adoptionPending;
|
|
580
|
+
delete ios.parkedCacheKey;
|
|
581
|
+
saveConfig(cfg);
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
async function configureOwnedIosSim({ record, projectPath, slot, profile, out, reconcileIosSimulator }) {
|
|
585
|
+
if (profile) out(chalk.dim(phaseLine("device", `applying the SimSlim profile to ${record.deviceUdid}`)));
|
|
586
|
+
else if (record.simslimManaged) out(chalk.dim(phaseLine("device", `restoring stock simulator services on ${record.deviceUdid}`)));
|
|
587
|
+
const previouslyManaged = Boolean(record.simslimManaged);
|
|
588
|
+
if (profile && !record.simslimManaged) {
|
|
589
|
+
const pending = {
|
|
590
|
+
...record,
|
|
591
|
+
simslimManaged: true
|
|
592
|
+
};
|
|
593
|
+
setDevice(projectPath, "ios", pending, slot);
|
|
594
|
+
record = pending;
|
|
595
|
+
}
|
|
596
|
+
const result = await reconcileIosSimulator({
|
|
597
|
+
udid: record.deviceUdid,
|
|
598
|
+
profile,
|
|
599
|
+
previouslyManaged,
|
|
600
|
+
out
|
|
601
|
+
});
|
|
602
|
+
const updated = { ...record };
|
|
603
|
+
if (result.managed) updated.simslimManaged = true;
|
|
604
|
+
else delete updated.simslimManaged;
|
|
605
|
+
setDevice(projectPath, "ios", updated, slot);
|
|
606
|
+
return updated;
|
|
607
|
+
}
|
|
608
|
+
async function ensureOwnedAndroidDevice({ record, projectPath, slot, settingsRoot, label, settings, flags, note, out, logFile, alive, configureAvd, teardownAvd }) {
|
|
609
|
+
const avdConfig = androidAvdConfigSetting(settings, settingsRoot);
|
|
610
|
+
const configuration = avdPoolConfiguration(androidDataPartitionSizeGbSetting(settings), avdConfig);
|
|
611
|
+
if (record?.setupIncomplete && record.avdName) {
|
|
612
|
+
const avdName = record.avdName;
|
|
613
|
+
const cleanup = teardownAvd(avdName, {
|
|
614
|
+
del: true,
|
|
615
|
+
owner: {
|
|
616
|
+
projectPath,
|
|
617
|
+
slot,
|
|
618
|
+
expectedRecord: record
|
|
619
|
+
},
|
|
620
|
+
onRemoved: () => {
|
|
621
|
+
clearDevice(projectPath, "android", slot, avdName);
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
if (cleanup.status === "failed" || cleanup.status === "skipped") throw new AvdRecoveryError(`Owned AVD ${record.avdName} has incomplete setup and could not be deleted (${cleanup.reason || cleanup.status}). Fix the cause, then retry; Stim kept the device record for cleanup.`, "Wait for any active AVD operation to finish. If the refusal names an unresolved claim, inspect its creator and native processes before removing only that claim. Retry `stim android` to reconcile the incomplete device.");
|
|
625
|
+
record = null;
|
|
626
|
+
}
|
|
627
|
+
if (record?.avdName) {
|
|
628
|
+
if (record.owned) {
|
|
629
|
+
const resolved = resolveOwnedAvdSerial(record.avdName);
|
|
630
|
+
if (resolved.notOwned) note(chalk.yellow(`Note: recorded AVD ${record.avdName} is not Stim-owned by name -- creating a fresh owned AVD instead of reusing it.`));
|
|
631
|
+
else if (resolved.serial) {
|
|
632
|
+
const consolePort = Number(resolved.serial.replace(/^emulator-/, ""));
|
|
633
|
+
if (record.consolePort && record.consolePort !== consolePort) out(phaseLine("device", `${record.avdName} changed serial (emulator-${record.consolePort} -> ${resolved.serial}); reconnecting this run, reopen agent-device on ${resolved.serial}`));
|
|
634
|
+
const updated = {
|
|
635
|
+
...record,
|
|
636
|
+
avdName: record.avdName,
|
|
637
|
+
consolePort,
|
|
638
|
+
owned: true,
|
|
639
|
+
deviceName: record.deviceName ?? record.avdName
|
|
640
|
+
};
|
|
641
|
+
setDevice(projectPath, "android", updated, slot);
|
|
642
|
+
return {
|
|
643
|
+
...updated,
|
|
644
|
+
systemImage: ownedAvdSystemImage(record.avdName)
|
|
645
|
+
};
|
|
646
|
+
} else if (!resolved.missing) {
|
|
647
|
+
out(chalk.dim(`Recorded port for owned AVD ${record.avdName} is not currently ours; booting it on a freshly allocated port...`));
|
|
648
|
+
return {
|
|
649
|
+
...await bootOwnedAvdOnFreshPort({
|
|
650
|
+
avdName: record.avdName,
|
|
651
|
+
metadata: record,
|
|
652
|
+
projectPath,
|
|
653
|
+
slot,
|
|
654
|
+
deviceName: record.deviceName,
|
|
655
|
+
out,
|
|
656
|
+
logFile,
|
|
657
|
+
alive
|
|
658
|
+
}),
|
|
659
|
+
systemImage: ownedAvdSystemImage(record.avdName)
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
} else if (listAvds().includes(record.avdName)) {
|
|
663
|
+
if (!listAdbDevices().emulators.some((e) => e.consolePort === record.consolePort)) {
|
|
664
|
+
note(chalk.yellow(`Note: assigned AVD ${record.avdName} (emulator-${record.consolePort}) is shut down and is not owned by Stim, so it will not be booted automatically.`));
|
|
665
|
+
note(chalk.dim("Boot it yourself, or run `stim gc --delete` to clear the assignment so Stim can create an owned AVD."));
|
|
666
|
+
}
|
|
667
|
+
return record;
|
|
668
|
+
}
|
|
669
|
+
} else if (record?.serial) {
|
|
670
|
+
note(chalk.yellow(`Note: this project has a stored assignment to physical device ${record.serial}, which Stim no longer keeps.`));
|
|
671
|
+
note(chalk.dim("Creating an owned emulator instead. Pass `--device` to build for a connected device."));
|
|
672
|
+
}
|
|
673
|
+
if (parkedMaxSetting("android").max > 0) {
|
|
674
|
+
const systemImage = pickDefaultSystemImage(listInstalledSystemImages(), { systemImage: flags.systemImage || settings.android?.systemImage })?.pkg;
|
|
675
|
+
const candidates = readParked("android").filter((entry) => entry.systemImage === systemImage && entry.configuration === configuration).toSorted((a, b) => a.parkedAt.localeCompare(b.parkedAt));
|
|
676
|
+
for (const parked of candidates) {
|
|
677
|
+
if (isLegacyDeletionClaim(parked.deletionClaim)) continue;
|
|
678
|
+
const resolved = resolveOwnedAvdSerial(parked.name);
|
|
679
|
+
if (resolved.missing) {
|
|
680
|
+
const result = teardownParkedAvd(parked.name);
|
|
681
|
+
if (result.status === "failed") out(phaseLine("device", `kept ${parked.name}: ${result.reason}`));
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
if (resolved.notOwned || resolved.serial) continue;
|
|
685
|
+
try {
|
|
686
|
+
assertOwnedAvdStopped(parked.name);
|
|
687
|
+
} catch (error) {
|
|
688
|
+
out(phaseLine("device", `kept parked ${parked.name}: ${String(error?.message || error)}`));
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
if (ownedAvdSystemImage(parked.name) !== systemImage || !ownedAvdMatchesConfiguration(parked.name, configuration)) {
|
|
692
|
+
const result = teardownParkedAvd(parked.name);
|
|
693
|
+
if (result.status === "failed") out(phaseLine("device", `kept ${parked.name}: ${result.reason}`));
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
const adopted = {
|
|
697
|
+
avdName: parked.name,
|
|
698
|
+
deviceName: parked.name,
|
|
699
|
+
owned: true,
|
|
700
|
+
poolConfiguration: configuration,
|
|
701
|
+
adoptionPending: true
|
|
702
|
+
};
|
|
703
|
+
if (!adoptParked({
|
|
704
|
+
platform: "android",
|
|
705
|
+
projectPath,
|
|
706
|
+
slot,
|
|
707
|
+
udid: parked.udid,
|
|
708
|
+
device: adopted
|
|
709
|
+
})) continue;
|
|
710
|
+
return {
|
|
711
|
+
...await bootOwnedAvdOnFreshPort({
|
|
712
|
+
avdName: parked.name,
|
|
713
|
+
projectPath,
|
|
714
|
+
slot,
|
|
715
|
+
metadata: adopted,
|
|
716
|
+
out,
|
|
717
|
+
logFile,
|
|
718
|
+
alive
|
|
719
|
+
}),
|
|
720
|
+
adopted: true,
|
|
721
|
+
systemImage
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
const created = await prepareOwnedAvd({
|
|
726
|
+
projectPath,
|
|
727
|
+
slot,
|
|
728
|
+
label,
|
|
729
|
+
previousAvdName: record?.avdName,
|
|
730
|
+
systemImage: flags.systemImage || settings.android?.systemImage || void 0,
|
|
731
|
+
configuration,
|
|
732
|
+
configure: (avdName) => configureAvd(avdName, {
|
|
733
|
+
dataPartitionSizeGb: androidDataPartitionSizeGbSetting(settings),
|
|
734
|
+
avdConfig
|
|
735
|
+
}),
|
|
736
|
+
teardown: teardownAvd
|
|
737
|
+
});
|
|
738
|
+
if (!created.created) {
|
|
739
|
+
out(chalk.dim(phaseLine("device", `recovered ${created.avdName} (unrecorded from a prior run)`)));
|
|
740
|
+
if (created.serial) return {
|
|
741
|
+
...created,
|
|
742
|
+
owned: true,
|
|
743
|
+
deviceName: created.avdName
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
return {
|
|
747
|
+
...await bootOwnedAvdOnFreshPort({
|
|
748
|
+
avdName: created.avdName,
|
|
749
|
+
metadata: created.created ? { poolConfiguration: configuration } : void 0,
|
|
750
|
+
projectPath,
|
|
751
|
+
slot,
|
|
752
|
+
deviceName: created.avdName,
|
|
753
|
+
out,
|
|
754
|
+
logFile,
|
|
755
|
+
alive
|
|
756
|
+
}),
|
|
757
|
+
created: created.created,
|
|
758
|
+
systemImage: created.systemImage
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
function claimAndroidConsolePort({ projectPath, slot, avdName, deviceName, livePorts = [], metadata }, { lock = withConfigLock, recordedPorts = () => allConsolePortsAndSerials().androidConsolePorts, record = setDevice } = {}) {
|
|
762
|
+
return lock(() => {
|
|
763
|
+
const consolePort = nextConsolePort([...recordedPorts(), ...livePorts]);
|
|
764
|
+
const claim = {
|
|
765
|
+
...metadata?.poolConfiguration ? { poolConfiguration: metadata.poolConfiguration } : {},
|
|
766
|
+
...metadata?.adoptionPending ? { adoptionPending: true } : {},
|
|
767
|
+
avdName,
|
|
768
|
+
consolePort,
|
|
769
|
+
owned: true,
|
|
770
|
+
deviceName: deviceName ?? avdName
|
|
771
|
+
};
|
|
772
|
+
record(projectPath, "android", claim, slot);
|
|
773
|
+
return claim;
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
function liveAndroidConsolePorts() {
|
|
777
|
+
const adbLive = listAdbDevices();
|
|
778
|
+
return [...adbLive.emulators.map((e) => e.consolePort), ...adbLive.unhealthy.map((u) => u.consolePort).filter((p) => p != null)];
|
|
779
|
+
}
|
|
780
|
+
async function bootOwnedAvdOnFreshPort({ avdName, metadata, projectPath, slot, deviceName, out, logFile = null, alive = pidExists }) {
|
|
781
|
+
const claim = claimAndroidConsolePort({
|
|
782
|
+
projectPath,
|
|
783
|
+
slot,
|
|
784
|
+
avdName,
|
|
785
|
+
deviceName,
|
|
786
|
+
livePorts: liveAndroidConsolePorts(),
|
|
787
|
+
metadata
|
|
788
|
+
});
|
|
789
|
+
const serial = `emulator-${claim.consolePort}`;
|
|
790
|
+
try {
|
|
791
|
+
reportAndroidMemoryPressure(out);
|
|
792
|
+
const pid = bootAndroidEmulator(avdName, claim.consolePort, { logFile });
|
|
793
|
+
out(chalk.dim(phaseLine("device", `waiting for ${serial} to finish booting`)));
|
|
794
|
+
const result = await waitForAndroidBoot({
|
|
795
|
+
serial,
|
|
796
|
+
timeoutMs: 12e4,
|
|
797
|
+
pid,
|
|
798
|
+
alive,
|
|
799
|
+
out
|
|
800
|
+
});
|
|
801
|
+
if (result.failed) throw new AvdBootError(result.reason, result.remedy);
|
|
802
|
+
const running = getAvdNameForSerial(serial);
|
|
803
|
+
if (running && running !== avdName) throw new Error(`${serial} is running AVD ${running}, not this workspace's owned AVD ${avdName}; refusing to use it.`);
|
|
804
|
+
return {
|
|
805
|
+
...claim,
|
|
806
|
+
serial
|
|
807
|
+
};
|
|
808
|
+
} catch (error) {
|
|
809
|
+
releaseAndroidConsolePort(projectPath, claim.consolePort, slot);
|
|
810
|
+
throw error;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
function reportAndroidMemoryPressure(out) {
|
|
814
|
+
const pressure = readHostMemoryPressure();
|
|
815
|
+
if (pressure === "warning" || pressure === "critical") out(chalk.dim(phaseLine("memory", `macOS reports ${pressure} host memory pressure; emulator boot may be slow. Free memory with \`stim stop\` only in workspaces you own; ask before closing other apps or devices.`)));
|
|
816
|
+
}
|
|
817
|
+
function androidBootRemedy(pressure) {
|
|
818
|
+
let count = null;
|
|
819
|
+
try {
|
|
820
|
+
count = liveOwnedDeviceCount({
|
|
821
|
+
sims: process.platform === "darwin" ? listAllIosSims({ timeoutMs: 2e3 }) : [],
|
|
822
|
+
adbEmulators: listAdbDevices({ timeoutMs: 2e3 }).emulators,
|
|
823
|
+
config: loadConfig()
|
|
824
|
+
});
|
|
825
|
+
} catch {}
|
|
826
|
+
const observation = pressure === "warning" || pressure === "critical" ? `macOS reports ${pressure} host memory pressure. ` : "";
|
|
827
|
+
const devices = count ? `${count} Stim-owned ${count === 1 ? "device is" : "devices are"} running. ` : "";
|
|
828
|
+
if (observation || devices) return `${observation}${devices}Stop an unneeded device with \`stim stop\` only in a workspace you own, then run \`stim android\` again; ask before closing other apps or devices. These observations do not establish the cause of the timeout or an OOM crash.`;
|
|
829
|
+
return "Inspect the emulator log and run `stim doctor` to check JAVA_HOME, ANDROID_HOME, and installed system images, then run `stim android` again.";
|
|
830
|
+
}
|
|
831
|
+
async function waitForAndroidBoot({ serial, timeoutMs, pid = null, alive = pidExists, out }) {
|
|
832
|
+
const aborted = () => pid !== null && !alive(pid);
|
|
833
|
+
const wait = (windowMs) => waitForBoot(serial, windowMs, {
|
|
834
|
+
aborted,
|
|
835
|
+
commandTimeoutMs: 5e3
|
|
836
|
+
});
|
|
837
|
+
let result = await wait(timeoutMs);
|
|
838
|
+
if (result.ok) return {
|
|
839
|
+
ok: true,
|
|
840
|
+
serial
|
|
841
|
+
};
|
|
842
|
+
let pressure = readHostMemoryPressure();
|
|
843
|
+
let elapsedMs = timeoutMs;
|
|
844
|
+
if (!result.exited && pid !== null && !aborted() && (pressure === "warning" || pressure === "critical")) {
|
|
845
|
+
const extensionMs = 24e4;
|
|
846
|
+
out(chalk.dim(phaseLine("memory", `macOS reports ${pressure} host memory pressure; retrying the boot wait once for the same live emulator ${serial} (up to ${extensionMs / 1e3}s more).`)));
|
|
847
|
+
result = await wait(extensionMs);
|
|
848
|
+
if (result.ok) return {
|
|
849
|
+
ok: true,
|
|
850
|
+
serial
|
|
851
|
+
};
|
|
852
|
+
elapsedMs += extensionMs;
|
|
853
|
+
pressure = readHostMemoryPressure();
|
|
854
|
+
}
|
|
855
|
+
const exited = result.exited || aborted();
|
|
856
|
+
return {
|
|
857
|
+
failed: true,
|
|
858
|
+
reason: `${exited ? `The emulator process for ${serial} exited before the device finished booting.` : `Emulator ${serial} did not finish booting within ${Math.round(elapsedMs / 1e3)}s.`} Diagnostic: ${JSON.stringify(result.diagnostic)}`,
|
|
859
|
+
remedy: exited ? "Inspect the emulator log for the process exit, fix what it reports, then run `stim android` again." : androidBootRemedy(pressure)
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
function liveOwnedDeviceCount({ sims = [], adbEmulators = [], config = null } = {}) {
|
|
863
|
+
let count = 0;
|
|
864
|
+
for (const sim of sims) if (sim?.state === "Booted" && sim.name?.startsWith("stim-")) count++;
|
|
865
|
+
const livePorts = new Set(adbEmulators.map((e) => e.consolePort));
|
|
866
|
+
for (const proj of Object.values(config?.projects || {})) for (const { platforms } of projectDeviceSlots(proj)) {
|
|
867
|
+
const android = platforms.android;
|
|
868
|
+
if (android?.owned && android.avdName && typeof android.consolePort === "number" && livePorts.has(android.consolePort)) count++;
|
|
869
|
+
}
|
|
870
|
+
return count;
|
|
871
|
+
}
|
|
872
|
+
function workspaceHasLiveDevice({ platform, project, slot = "default", sims = [], adbEmulators = [] } = {}) {
|
|
873
|
+
if (!platform) return false;
|
|
874
|
+
const record = deviceSlotPlatforms(project, slot)?.[platform];
|
|
875
|
+
if (!record) return false;
|
|
876
|
+
if (platform === "ios") return sims.some((s) => s.udid === record.deviceUdid && s.state === "Booted");
|
|
877
|
+
return typeof record.consolePort === "number" && adbEmulators.some((e) => e.consolePort === record.consolePort);
|
|
878
|
+
}
|
|
879
|
+
function deviceCapacityRefusal({ platform, project, slot = "default", max, sims = [], adb = null, config = null } = {}) {
|
|
880
|
+
if (!max || max <= 0) return null;
|
|
881
|
+
const adbEmulators = adb?.emulators || [];
|
|
882
|
+
if (workspaceHasLiveDevice({
|
|
883
|
+
platform,
|
|
884
|
+
project,
|
|
885
|
+
slot,
|
|
886
|
+
sims,
|
|
887
|
+
adbEmulators
|
|
888
|
+
})) return null;
|
|
889
|
+
const count = liveOwnedDeviceCount({
|
|
890
|
+
sims,
|
|
891
|
+
adbEmulators,
|
|
892
|
+
config
|
|
893
|
+
});
|
|
894
|
+
if (count < max) return null;
|
|
895
|
+
return {
|
|
896
|
+
code: "STIM_AT_CAPACITY",
|
|
897
|
+
message: `${count} Stim device(s) are already booted and concurrency.maxDevices is ${max}, so booting another would exceed the cap.`,
|
|
898
|
+
remedy: "stop an environment (stim stop) or raise concurrency.maxDevices"
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
function checkDeviceCapacity({ platform, project, slot = "default", max, sims = listAllIosSims, adb = listAdbDevices, config = loadConfig } = {}) {
|
|
902
|
+
if (!max || max <= 0) return null;
|
|
903
|
+
let simList = [];
|
|
904
|
+
let adbRes = {
|
|
905
|
+
emulators: [],
|
|
906
|
+
physical: [],
|
|
907
|
+
unhealthy: []
|
|
908
|
+
};
|
|
909
|
+
try {
|
|
910
|
+
simList = typeof sims === "function" ? sims() || [] : sims || [];
|
|
911
|
+
} catch {}
|
|
912
|
+
try {
|
|
913
|
+
adbRes = typeof adb === "function" ? adb() || adbRes : adb || adbRes;
|
|
914
|
+
} catch {}
|
|
915
|
+
let cfg = null;
|
|
916
|
+
try {
|
|
917
|
+
cfg = typeof config === "function" ? config() : config ?? null;
|
|
918
|
+
} catch {}
|
|
919
|
+
return deviceCapacityRefusal({
|
|
920
|
+
platform,
|
|
921
|
+
project,
|
|
922
|
+
slot,
|
|
923
|
+
max,
|
|
924
|
+
sims: simList,
|
|
925
|
+
adb: adbRes,
|
|
926
|
+
config: cfg
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
function deviceTypeMismatch(recordedTypeId, requestedName, deviceTypes) {
|
|
930
|
+
if (!requestedName || !recordedTypeId) return null;
|
|
931
|
+
const wanted = (deviceTypes || []).find((d) => d.name === requestedName);
|
|
932
|
+
if (!wanted) return null;
|
|
933
|
+
if (wanted.identifier === recordedTypeId) return null;
|
|
934
|
+
const recorded = (deviceTypes || []).find((d) => d.identifier === recordedTypeId);
|
|
935
|
+
return `this project's sim is ${recorded ? recorded.name : recordedTypeId}, but --device-type asked for ${requestedName}`;
|
|
936
|
+
}
|
|
937
|
+
function installedNames(names) {
|
|
938
|
+
const unique = [...new Set(names.filter((n) => typeof n === "string" && n !== ""))];
|
|
939
|
+
return unique.length > 0 ? unique.join(", ") : "none";
|
|
940
|
+
}
|
|
941
|
+
function unknownIosRuntimeRefusal(requested, runtimes) {
|
|
942
|
+
if (!requested) return null;
|
|
943
|
+
if ((runtimes || []).some((r) => iosRuntimeMatches(r, requested))) return null;
|
|
944
|
+
return {
|
|
945
|
+
message: `No installed simulator runtime matches "${requested}". Installed runtimes: ${installedNames((runtimes || []).map((r) => r.version))}.`,
|
|
946
|
+
remedy: "Pass `--runtime` (or set ios.runtime) a version printed above (\"26.5\") or a runtime's full name (\"iOS 26.5\"); nothing else matches. Install more runtimes through Xcode."
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
function creatableIosDeviceTypeNames(runtimes, runtime) {
|
|
950
|
+
return (runtime ? (runtimes || []).filter((r) => iosRuntimeMatches(r, runtime)) : runtimes || []).flatMap((r) => (r.supportedDeviceTypes || []).map((d) => d.name));
|
|
951
|
+
}
|
|
952
|
+
function unknownIosDeviceTypeRefusal(requested, runtimes, runtime) {
|
|
953
|
+
if (!requested) return null;
|
|
954
|
+
const creatable = creatableIosDeviceTypeNames(runtimes, runtime);
|
|
955
|
+
if (creatable.includes(requested)) return null;
|
|
956
|
+
return {
|
|
957
|
+
message: `No device type named "${requested}" can be created on ${runtime ? `runtime ${runtime}` : "any installed simulator runtime"}. ${runtime ? `Device types runtime ${runtime} supports` : "Device types the installed runtimes support"}: ${installedNames(creatable)}.`,
|
|
958
|
+
remedy: "Pass `--device-type` (or set ios.deviceType) one of the names printed above, exactly as `xcrun simctl list devicetypes` spells it. `xcrun simctl list devicetypes` also lists watchOS, tvOS and visionOS models, which no iOS runtime can create. Install more models through Xcode."
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
function unknownAndroidSystemImageRefusal(requested, images) {
|
|
962
|
+
if (!requested) return null;
|
|
963
|
+
if ((images || []).some((i) => i.pkg === requested)) return null;
|
|
964
|
+
return {
|
|
965
|
+
message: `No installed Android system image is named "${requested}". Installed system images: ${installedNames((images || []).map((i) => i.pkg))}.`,
|
|
966
|
+
remedy: "Pass `--system-image` (or set android.systemImage) to one of the package ids printed above. Install more with `sdkmanager \"system-images;android-36;google_apis;arm64-v8a\"`."
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
const BOOT_POLL_MS = 500;
|
|
970
|
+
async function ensureBooted({ platform, device, timeoutMs, pollMs = BOOT_POLL_MS, out = () => {}, logFile = null, alive = pidExists } = {}) {
|
|
971
|
+
if (platform === "ios") return ensureIosBooted({
|
|
972
|
+
device,
|
|
973
|
+
timeoutMs: timeoutMs ?? 6e5,
|
|
974
|
+
pollMs,
|
|
975
|
+
out
|
|
976
|
+
});
|
|
977
|
+
if (platform === "android") return ensureAndroidBooted({
|
|
978
|
+
device,
|
|
979
|
+
timeoutMs: timeoutMs ?? 24e4,
|
|
980
|
+
out,
|
|
981
|
+
logFile,
|
|
982
|
+
alive
|
|
983
|
+
});
|
|
984
|
+
return {
|
|
985
|
+
failed: true,
|
|
986
|
+
reason: `Unknown platform "${platform}".`
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
async function ensureIosBooted({ device, timeoutMs, pollMs, out }) {
|
|
990
|
+
const udid = device?.deviceUdid;
|
|
991
|
+
if (!udid) return {
|
|
992
|
+
failed: true,
|
|
993
|
+
reason: "No iOS simulator is recorded for this project."
|
|
994
|
+
};
|
|
995
|
+
const ready = () => {
|
|
996
|
+
try {
|
|
997
|
+
getExecutor().runFile("xcrun", [
|
|
998
|
+
"simctl",
|
|
999
|
+
"spawn",
|
|
1000
|
+
udid,
|
|
1001
|
+
"launchctl",
|
|
1002
|
+
"list"
|
|
1003
|
+
], { timeoutMs: 3e4 });
|
|
1004
|
+
return {
|
|
1005
|
+
ok: true,
|
|
1006
|
+
udid
|
|
1007
|
+
};
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
return {
|
|
1010
|
+
failed: true,
|
|
1011
|
+
reason: `Simulator ${udid} failed its process-spawn readiness check: ${error?.message || error}. ${iosSimulatorFailureAdvice()}`
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
const booting = device?.booting;
|
|
1016
|
+
if (booting?.udid === udid) {
|
|
1017
|
+
try {
|
|
1018
|
+
await booting.done;
|
|
1019
|
+
} catch (e) {
|
|
1020
|
+
return {
|
|
1021
|
+
failed: true,
|
|
1022
|
+
reason: `Could not boot simulator ${udid}: ${e?.message || e}`
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
return ready();
|
|
1026
|
+
}
|
|
1027
|
+
let resolved;
|
|
1028
|
+
try {
|
|
1029
|
+
resolved = resolveOwnedIosSim(udid);
|
|
1030
|
+
} catch (e) {
|
|
1031
|
+
return {
|
|
1032
|
+
failed: true,
|
|
1033
|
+
reason: `Could not list simulators: ${e?.message || e}`
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
if (resolved.missing) return {
|
|
1037
|
+
failed: true,
|
|
1038
|
+
reason: `Simulator ${udid} no longer exists. Run \`stim ios\` again to create a fresh owned sim.`
|
|
1039
|
+
};
|
|
1040
|
+
if (resolved.notOwned) return {
|
|
1041
|
+
failed: true,
|
|
1042
|
+
reason: `Simulator ${udid} is now named "${resolved.notOwned}" and is not Stim-owned; refusing to boot it.`
|
|
1043
|
+
};
|
|
1044
|
+
const sim = resolved.sim;
|
|
1045
|
+
if (sim.state === "Booted") return ready();
|
|
1046
|
+
out(chalk.dim(phaseLine("device", `booting ${sim.name} (${udid})`)));
|
|
1047
|
+
const bootDeadline = Date.now() + timeoutMs;
|
|
1048
|
+
try {
|
|
1049
|
+
await bootIosSim(udid, {
|
|
1050
|
+
timeoutMs,
|
|
1051
|
+
label: sim.name,
|
|
1052
|
+
out
|
|
1053
|
+
});
|
|
1054
|
+
} catch (e) {
|
|
1055
|
+
return {
|
|
1056
|
+
failed: true,
|
|
1057
|
+
reason: `Could not boot simulator ${udid}: ${e?.message || e}`
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
const deadline = Math.max(bootDeadline, Date.now() + 2 * pollMs);
|
|
1061
|
+
while (Date.now() < deadline) {
|
|
1062
|
+
await sleep(pollMs);
|
|
1063
|
+
let state = null;
|
|
1064
|
+
try {
|
|
1065
|
+
state = listAllIosSims({ timeoutMs: 3e4 }).find((s) => s.udid === udid)?.state ?? null;
|
|
1066
|
+
} catch {}
|
|
1067
|
+
if (state === "Booted") return ready();
|
|
1068
|
+
}
|
|
1069
|
+
return {
|
|
1070
|
+
failed: true,
|
|
1071
|
+
reason: `Simulator ${udid} did not reach the Booted state within ${Math.round(timeoutMs / 1e3)}s.`
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
async function ensureAndroidBooted({ device, timeoutMs, out, logFile = null, alive = pidExists }) {
|
|
1075
|
+
if (!device?.avdName) return {
|
|
1076
|
+
failed: true,
|
|
1077
|
+
reason: "No owned Android emulator is recorded for this project."
|
|
1078
|
+
};
|
|
1079
|
+
let resolved;
|
|
1080
|
+
try {
|
|
1081
|
+
resolved = resolveOwnedAvdSerial(device.avdName);
|
|
1082
|
+
} catch (e) {
|
|
1083
|
+
return {
|
|
1084
|
+
failed: true,
|
|
1085
|
+
reason: `Could not list AVDs: ${e?.message || e}`
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
if (resolved.missing) return {
|
|
1089
|
+
failed: true,
|
|
1090
|
+
reason: `AVD ${device.avdName} no longer exists. Run \`stim android\` again to create a fresh owned AVD.`
|
|
1091
|
+
};
|
|
1092
|
+
if (resolved.notOwned) return {
|
|
1093
|
+
failed: true,
|
|
1094
|
+
reason: `AVD ${device.avdName} is not Stim-owned by name; refusing to boot it.`
|
|
1095
|
+
};
|
|
1096
|
+
if (resolved.serial) return waitForAndroidBoot({
|
|
1097
|
+
serial: resolved.serial,
|
|
1098
|
+
timeoutMs,
|
|
1099
|
+
out
|
|
1100
|
+
});
|
|
1101
|
+
const freshSerial = `emulator-${device.consolePort}`;
|
|
1102
|
+
if (device.owned && device.serial === freshSerial) return waitForAndroidBoot({
|
|
1103
|
+
serial: freshSerial,
|
|
1104
|
+
timeoutMs,
|
|
1105
|
+
out
|
|
1106
|
+
});
|
|
1107
|
+
const serial = `emulator-${pickConsolePort(device.consolePort)}`;
|
|
1108
|
+
reportAndroidMemoryPressure(out);
|
|
1109
|
+
out(chalk.dim(phaseLine("device", `booting ${device.avdName} as ${serial}`)));
|
|
1110
|
+
let pid = null;
|
|
1111
|
+
try {
|
|
1112
|
+
pid = bootAndroidEmulator(device.avdName, Number(serial.replace(/^emulator-/, "")), { logFile });
|
|
1113
|
+
} catch (e) {
|
|
1114
|
+
return {
|
|
1115
|
+
failed: true,
|
|
1116
|
+
reason: `Could not start emulator for AVD ${device.avdName}: ${e?.message || e}`
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
return waitForAndroidBoot({
|
|
1120
|
+
serial,
|
|
1121
|
+
timeoutMs,
|
|
1122
|
+
pid,
|
|
1123
|
+
alive,
|
|
1124
|
+
out
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
function pickConsolePort(recorded) {
|
|
1128
|
+
const live = liveAndroidConsolePorts();
|
|
1129
|
+
if (recorded && !live.includes(Number(recorded))) return Number(recorded);
|
|
1130
|
+
return nextConsolePort([...allConsolePortsAndSerials().androidConsolePorts, ...live]);
|
|
1131
|
+
}
|
|
1132
|
+
function sleep(ms) {
|
|
1133
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
1134
|
+
}
|
|
1135
|
+
//#endregion
|
|
1136
|
+
export { liveOwnedDeviceCount as a, unknownIosRuntimeRefusal as c, simslimIsOnPath as d, ensureOwnedDevice as i, AvdBootError as l, clearIosAdoptionPending as n, unknownAndroidSystemImageRefusal as o, ensureBooted as r, unknownIosDeviceTypeRefusal as s, checkDeviceCapacity as t, AvdRecoveryError as u };
|