stim 1.18.0 → 1.20.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 +21 -4
- package/dist/{activity-BxnkDZDi.mjs → activity-BrGh9EAI.mjs} +3 -3
- package/dist/{agent-device-usage-output-CZ66YA4T.mjs → agent-device-usage-output-DVyv_hWj.mjs} +7 -7
- package/dist/android-0vv-OruW.mjs +6 -0
- package/dist/android-Brx6EKvJ.mjs +1258 -0
- package/dist/{android-DC3ZLuw0.mjs → android-BsYNR81W.mjs} +103 -45
- package/dist/{android-CBwCNicO.mjs → android-CK3fpEyZ.mjs} +2 -2
- package/dist/{android-cas-BBFyr9CA.mjs → android-cas-DCAuXryb.mjs} +9 -5
- package/dist/android-cas-compiler.mjs +1 -1
- package/dist/api-run.mjs +159 -8
- package/dist/{api-D0VtFhqk.mjs → api-zuX3ynYN.mjs} +9 -7
- package/dist/api.d.mts +104 -4
- package/dist/api.mjs +1 -1
- package/dist/{app-install-D-PUENug.mjs → app-install-0LgaQRT7.mjs} +39 -18
- package/dist/artifact-CnC0mfov.mjs +807 -0
- package/dist/artifact-DClXJNWx.mjs +772 -0
- package/dist/artifact-lifecycle-DzPpBYvK.mjs +30 -0
- package/dist/{web-DTU0uiWj.mjs → browser-web-CzBmhmgs.mjs} +136 -174
- package/dist/{budget-DIhL2BKx.mjs → budget-BONq9vXY.mjs} +3096 -3244
- package/dist/build-BixRI6sB.mjs +462 -0
- package/dist/build-plan-CP0tckNU.mjs +553 -0
- package/dist/{cache-manifest-BI7ozfmH.mjs → cache-manifest-qA3dT2x2.mjs} +1 -1
- package/dist/cache-manifest.mjs +1 -1
- package/dist/ccache-CjHQ5F8g.mjs +142 -0
- package/dist/{chrome-DJnmnS5q.mjs → chrome-Dp2lMLBe.mjs} +1 -1
- package/dist/{cli-DNJMwADx.mjs → cli-B2R8wH1F.mjs} +20 -20
- package/dist/cli.mjs +1 -1
- package/dist/{client-DzhapoMB.mjs → client-BLy1Pw2V.mjs} +172 -165
- package/dist/collector-run.d.mts +10 -0
- package/dist/collector-run.mjs +7 -7
- package/dist/{command-output-CBtzyIgF.mjs → command-output-BjPN99ZB.mjs} +2 -2
- package/dist/{config-CgYMahz7.mjs → config-D0LhTY3S.mjs} +13 -13
- package/dist/{created-devices-DKbeWsiK.mjs → created-devices-HU_DAb_Q.mjs} +7 -6
- package/dist/{dependency-state-xKoOypBm.mjs → dependency-state-B2m8eqsS.mjs} +2 -2
- package/dist/{detached-entry-BLfcgc4O.mjs → detached-entry-BXq4fIl-.mjs} +1 -1
- package/dist/{dev-client-CLnIH02x.mjs → dev-client-ClATaJoH.mjs} +2 -2
- package/dist/{device-D7oCHGWi.mjs → device-B7lsC0K1.mjs} +8 -8
- package/dist/device-DVCwAFlX.mjs +727 -0
- package/dist/{device-capacity-EUOOYr8T.mjs → device-capacity-Ca_ke1LE.mjs} +25 -15
- package/dist/device-host-worker.mjs +38 -18
- package/dist/{device-ios-6MewKKnc.mjs → device-ios-CRUqPxKw.mjs} +12 -11
- package/dist/{device-lease-Co-EwifR.mjs → device-lease-DHOBXGgS.mjs} +135 -22
- package/dist/{device-lease-run-w1xhCqIW.mjs → device-lease-run-CvDeHTkG.mjs} +4 -4
- package/dist/{device-pool-F5WvRPtD.mjs → device-pool-L2-5GxMy.mjs} +3 -3
- package/dist/{device-remote-tHbd1fKy.mjs → device-remote-Bmg42063.mjs} +17 -69
- package/dist/doctor-C2MSuMMg.mjs +515 -0
- package/dist/{doctor-B9ckYUoU.mjs → doctor-DPAqhVCK.mjs} +334 -721
- package/dist/doctor-watchman-CB4NnVZm.mjs +260 -0
- package/dist/{simslim-EHBQ2R4G.mjs → eas-build-Dw_ec2Km.mjs} +6 -152
- package/dist/{stim-installations-aR_WsNOA.mjs → eas-session-ledger-DZlC-Fq3.mjs} +57 -4
- package/dist/{error-diagnostics-BRXOqDzG.mjs → error-diagnostics-Du7I_GuY.mjs} +17 -12
- package/dist/errors-DzkMYh8Q.mjs +205 -0
- package/dist/{exec-DWhCZDGm.mjs → exec-B81Qodjn.mjs} +69 -15
- package/dist/{gc-Dh6mXOKC.mjs → gc-PWp34zGb.mjs} +23 -22
- package/dist/{gradle-C6GKnunQ.mjs → gradle-C2OEKCCI.mjs} +50 -125
- package/dist/{guide-tm6igAaa.mjs → guide-Dai9lphv.mjs} +862 -538
- package/dist/{hosted-android-Bv285t_t.mjs → hosted-android-DQ-Agsd8.mjs} +5 -5
- package/dist/{hosted-client-C-rMtppF.mjs → hosted-client-Io01s5By.mjs} +9 -3
- package/dist/{hosted-ios-D5mNmtj4.mjs → hosted-ios-DIxFnYp-.mjs} +3 -3
- package/dist/{hosted-logs-DN8Iz4mJ.mjs → hosted-logs-DP-ABU_M.mjs} +8 -8
- package/dist/{hosted-macos-B-wEjlwG.mjs → hosted-macos-Aziw9T_9.mjs} +7 -6
- package/dist/{hosted-native-DeeOG8xZ.mjs → hosted-native-BdAQi21M.mjs} +23 -16
- package/dist/{idle-shutdown-jjtxAn5C.mjs → idle-shutdown-DdMqoSUB.mjs} +7 -7
- package/dist/ios-BZ9dg2Fr.mjs +9 -0
- package/dist/{ios-B3SbOWdw.mjs → ios-CXLIUxXJ.mjs} +180 -24
- package/dist/ios-CZuv6gRv.mjs +1112 -0
- package/dist/{ios-Bvbf0SGY.mjs → ios-VGvNdsv2.mjs} +1 -1
- package/dist/{ios-device-Bn7jWDB1.mjs → ios-device-B1B14xND.mjs} +1 -1
- package/dist/{ios-device-vOaByMhw.mjs → ios-device-BreZOpM0.mjs} +3 -3
- package/dist/ios-project-B9drt_aY.mjs +197 -0
- package/dist/{ios-state-B2pES6Vs.mjs → ios-state-CjrL2qv-.mjs} +1 -1
- package/dist/js-swap-C8icNYIY.mjs +432 -0
- package/dist/launch-DxH0THLP.mjs +745 -0
- package/dist/launch-YrbqaIFj.mjs +1530 -0
- package/dist/{launch-verify-hskNcWkZ.mjs → launch-verify-BzRdbuO1.mjs} +26 -7
- package/dist/{logs-1Ylc1Ri6.mjs → logs-DHaRNfg7.mjs} +11 -11
- package/dist/macos-C0iljhsN.mjs +2 -0
- package/dist/{macos-DF6C7wUU.mjs → macos-V_3Ui7g4.mjs} +125 -155
- package/dist/macos-run.mjs +1 -1
- package/dist/maintenance-run.mjs +13 -10
- package/dist/{metro-Dh8T4Ytd.mjs → metro-D_j9sN-B.mjs} +209 -43
- package/dist/{metro-gateway-CEUw9RMH.mjs → metro-gateway-BBJldOdi.mjs} +2 -2
- package/dist/miss-reason-BPAzZS0r.mjs +238 -0
- package/dist/{named-ports-BFIz1Shz.mjs → named-ports-C2lvoPsb.mjs} +12 -5
- package/dist/native-android-lHzaew7r.mjs +226 -0
- package/dist/native-gradle-inputs-BQnIdAlL.mjs +356 -0
- package/dist/{native-runtime-D_603JiC.mjs → native-runtime-BRmWL7ru.mjs} +9 -8
- package/dist/native-xcode-inputs-eutS19jU.mjs +307 -0
- package/dist/native-xcode-ios-DP3cHLwu.mjs +434 -0
- package/dist/{ndjson-LGZAMsqa.mjs → ndjson-GOnbyvso.mjs} +6 -6
- package/dist/offload-worker.d.mts +12 -6
- package/dist/offload-worker.mjs +409 -73
- package/dist/{ownership-D6Se7gg_.mjs → ownership-DXzMMSx6.mjs} +2 -2
- package/dist/{placement-log-BcPhRwR4.mjs → placement-log-reFbjqEr.mjs} +7 -3
- package/dist/plan-BDk2AeY_.mjs +260 -0
- package/dist/plan-placement-D_YYlE6N.mjs +73 -0
- package/dist/{ports-BLTtfBq-.mjs → ports-jzvAJXXL.mjs} +3 -3
- package/dist/{prebuild-juCyreaR.mjs → prebuild-Bw44QxVh.mjs} +4 -4
- package/dist/{preview-D5V07Nrl.mjs → preview-BaBJ72Al.mjs} +51 -34
- package/dist/project-B8_duAzW.mjs +1067 -0
- package/dist/project-plan-BPwbLwQu.mjs +10 -0
- package/dist/{pull-request-u4vp7zmK.mjs → pull-request-vOhLNmIb.mjs} +1 -1
- package/dist/pull-requests.mjs +1 -1
- package/dist/react-native-android-chm1DMlA.mjs +921 -0
- package/dist/react-native-build-oAw6gQeW.mjs +239 -0
- package/dist/react-native-doctor-DYXoh-6I.mjs +78 -0
- package/dist/react-native-ios-SCTK_CT5.mjs +595 -0
- package/dist/{recordings-BAWltsVt.mjs → recordings-Bk-VivUV.mjs} +1 -1
- package/dist/{reload-DvyWzVjJ.mjs → reload-D34GkY78.mjs} +17 -14
- package/dist/remote-BU9UyhCJ.mjs +192 -0
- package/dist/{remote-cache-BGg7aGIZ.mjs → remote-cache-Bsts6f6Y.mjs} +5 -5
- package/dist/result-BDoRk1k3.mjs +186 -0
- package/dist/{run-vhtDgpHO.mjs → run-qvff78hv.mjs} +3 -3
- package/dist/{server-bare-B9E7S1aR.mjs → server-bare-dWjAarki.mjs} +6 -6
- package/dist/server-command-7XIKlWtO.mjs +69 -0
- package/dist/{server-expo-DoBHhuzK.mjs → server-expo-CsYu2aKL.mjs} +107 -82
- package/dist/server-expo-DV43vep4.mjs +2 -0
- package/dist/settings-Bbztm4oV.mjs +1503 -0
- package/dist/{settings-Byp2AQ_E.mjs → settings-CT3E71-i.mjs} +6 -7
- package/dist/{state-lKh8shBp.d.mts → settings-vk3mJGSm.d.mts} +4 -2
- package/dist/settings.schema.json +156 -6
- package/dist/simslim-ChMfe8rg.mjs +149 -0
- package/dist/{slot-launch-C1nqh4mu.mjs → slot-launch-B9hn96wd.mjs} +6 -6
- package/dist/{stage-g_WqWeaE.mjs → stage-BNKKdDNc.mjs} +6 -3
- package/dist/start-CbQCSnXQ.mjs +2 -0
- package/dist/{start-D-IeOyoK.mjs → start-CdvcTaXr.mjs} +113 -77
- package/dist/{state-xnqpx3AD.mjs → state-Boo0IMJq.mjs} +4 -2
- package/dist/{state-BG-aAk4k.mjs → state-DI84A-uu.mjs} +29 -10
- package/dist/{stats-DY6ekz8F.mjs → stats-BIIDrjA8.mjs} +6 -6
- package/dist/{status-BEeL6IKz.mjs → status-BDGHwkCs.mjs} +3 -1
- package/dist/{status-DCjZeE-K.mjs → status-DARMkc9q.mjs} +80 -294
- package/dist/status-watch-DvmI3SmN.mjs +296 -0
- package/dist/{stim-desktop-Behws_fi.mjs → stim-desktop-8GQX2OV8.mjs} +1 -1
- package/dist/{stop-DIJtD2GL.mjs → stop-CCZS-C9m.mjs} +2 -2
- package/dist/stop-WQPQHLuy.mjs +72 -0
- package/dist/{stop-BTjs3xbc.mjs → stop-iNemHsJC.mjs} +81 -33
- package/dist/supervisor-run.d.mts +7 -2
- package/dist/supervisor-run.mjs +32 -20
- package/dist/{support-Bwvbrq5-.mjs → support-CnE_SIjt.mjs} +17 -7
- package/dist/{support-BaayeYCf.mjs → support-DCco5fut.mjs} +61 -8
- package/dist/swiftpm-macos-afeNUExW.mjs +119 -0
- package/dist/{native-run-B06FkmZx.mjs → tailnet-CoJUb2tB.mjs} +113 -6
- package/dist/toolchain-BIupIro1.mjs +2 -0
- package/dist/{toolchain-Bnyd_K3A.mjs → toolchain-DqyDXs30.mjs} +225 -315
- package/dist/{trigger-CJMhwXe8.mjs → trigger-C0MWBi8O.mjs} +3 -3
- package/dist/trigger-SHLOWJ4O.mjs +2 -0
- package/dist/{warm-progress-CjB-MP6H.mjs → warm-progress-C3ASD6z5.mjs} +134 -37
- package/dist/{status-Dcr0Qexd.mjs → watchman-CQl51YZu.mjs} +75 -7
- package/dist/web-B3eldHFH.mjs +123 -0
- package/dist/web-BLXpOe6b.mjs +2 -0
- package/dist/web-run.mjs +10 -10
- package/dist/{workspace-state-C59dqG-Z.mjs → workspace-state-CSIFuuDa.mjs} +33 -33
- package/dist/{workspaces-xXItD8Dj.mjs → workspaces-D2xSHBDY.mjs} +1297 -1211
- package/dist/{worktree-BSYlofkq.mjs → worktree-BxDqaTev.mjs} +102 -63
- package/dist/worktree-q7_OZNY0.mjs +2 -0
- package/package.json +8 -6
- package/shim/bundle-response.cjs +2 -1
- package/shim/bundle-response.d.cts +1 -1
- package/shim/expo-metro-config.cjs +1 -1
- package/shim/native-android.gradle +101 -0
- package/dist/android-BKA16DTf.mjs +0 -4
- package/dist/android-BvwuMUJ9.mjs +0 -3572
- package/dist/guide-status-Bhf9SJXc.mjs +0 -125
- package/dist/ios-CPdkoKaa.mjs +0 -3953
- package/dist/ios-DNpdygiv.mjs +0 -7
- package/dist/macos-Ck7OBMNY.mjs +0 -2
- package/dist/metro-store-BAHFcJm6.mjs +0 -106
- package/dist/plan-placement-CtxkCO54.mjs +0 -1859
- package/dist/project-C3ChRLZy.mjs +0 -212
- package/dist/projects-DD7dBTCj.mjs +0 -92
- package/dist/server-expo-Db9HBUoa.mjs +0 -2
- package/dist/settings-CQyMElEP.mjs +0 -701
- package/dist/start-NTc1Ifqs.mjs +0 -2
- package/dist/stop-BGhKMOuH.mjs +0 -48
- package/dist/trigger-DWAq9hkw.mjs +0 -2
- package/dist/web-B6_bFDn7.mjs +0 -2
- package/dist/worktree-BJfnU6r5.mjs +0 -2
- package/dist/worktree-CsKYexJM.mjs +0 -753
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
import { n as ownership_claim_exports } from "./process-identity-Biv3arCO.mjs";
|
|
2
|
+
import { t as getExecutor } from "./exec-B81Qodjn.mjs";
|
|
3
|
+
import { D as withConfigLock, H as projectDeviceSlots, _ as loadConfig, a as clearDevice, b as releaseAndroidConsolePort, i as clearAndroidBootPending, k as acquireAvdClaim, t as allConsolePortsAndSerials, w as setDevice, z as deviceSlotPlatforms } from "./config-D0LhTY3S.mjs";
|
|
4
|
+
import { l as androidDataPartitionSizeGbSetting, o as androidAvdConfigSetting } from "./settings-Bbztm4oV.mjs";
|
|
5
|
+
import { l as phaseLine } from "./command-output-BjPN99ZB.mjs";
|
|
6
|
+
import { o as ownedDeviceLabel } from "./project-B8_duAzW.mjs";
|
|
7
|
+
import { A as listAvds, B as ownedAvdSystemImage, E as getAvdNameForSerial, F as ownedAvdDeviceProfile, J as resolveOwnedAvdSerial, L as ownedAvdMatchesConfiguration, O as listAdbDevices, P as nextConsolePort, R as ownedAvdName, W as pickDefaultSystemImage, b as createOwnedAvd, dt as parkedMaxSetting, f as assertOwnedAvdStopped, ft as readParked, g as avdPoolConfiguration, j as listInstalledSystemImages, lt as isLegacyDeletionClaim, n as OWNED_AVD_CONFIG_DEFAULTS, ot as adoptParked, t as DEFAULT_AVD_DEVICE_PROFILE, tt as waitForBoot, v as bootAndroidEmulator, y as configureNewOwnedAvd } from "./android-BsYNR81W.mjs";
|
|
8
|
+
import { s as pidExists } from "./metro-D_j9sN-B.mjs";
|
|
9
|
+
import { v as teardownOwnedAvd, x as teardownParkedAvd } from "./workspaces-D2xSHBDY.mjs";
|
|
10
|
+
import { F as memoryCulpritAdvice, N as memoryCulpritsUnder, P as readHostMemoryPressure, d as listAllIosSims } from "./ios-CXLIUxXJ.mjs";
|
|
11
|
+
import { t as clearDeviceIdleShutdown } from "./idle-shutdown-DdMqoSUB.mjs";
|
|
12
|
+
import { i as countLiveOwnedDevices, n as DeviceAdmissionRefusal, p as withDeviceBootAdmission } from "./device-capacity-Ca_ke1LE.mjs";
|
|
13
|
+
import { t as reconcileSimSlim } from "./simslim-ChMfe8rg.mjs";
|
|
14
|
+
import { i as ensureOwnedIosDevice, r as ensureIosBooted } from "./device-ios-CRUqPxKw.mjs";
|
|
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 { randomUUID } from "node:crypto";
|
|
18
|
+
import chalk from "chalk";
|
|
19
|
+
import { isDeepStrictEqual } from "node:util";
|
|
20
|
+
import { request } from "node:http";
|
|
21
|
+
//#region src/engine/metro-warmup.ts
|
|
22
|
+
const WARMUP_TIMEOUT_MS = 6e4;
|
|
23
|
+
async function requestLocal(url, platform, prefetch) {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
const req = request(url, { headers: {
|
|
26
|
+
accept: "application/json",
|
|
27
|
+
"expo-platform": platform,
|
|
28
|
+
...prefetch ? { "x-stim-metro-warmup": "1" } : {}
|
|
29
|
+
} });
|
|
30
|
+
const timer = setTimeout(() => req.destroy(), WARMUP_TIMEOUT_MS);
|
|
31
|
+
timer.unref();
|
|
32
|
+
req.on("socket", (socket) => socket.unref());
|
|
33
|
+
req.on("error", () => resolve(null));
|
|
34
|
+
req.on("close", () => {
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
resolve(null);
|
|
37
|
+
});
|
|
38
|
+
req.on("response", (res) => {
|
|
39
|
+
let body = "";
|
|
40
|
+
res.setEncoding("utf8");
|
|
41
|
+
res.on("data", (chunk) => {
|
|
42
|
+
if (!prefetch) body += chunk;
|
|
43
|
+
});
|
|
44
|
+
res.on("error", () => resolve(null));
|
|
45
|
+
res.on("end", () => resolve(res.statusCode === 200 ? body : null));
|
|
46
|
+
});
|
|
47
|
+
req.end();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async function warmMetro({ port, platform, isExpo, appId, bundleUrl }) {
|
|
51
|
+
try {
|
|
52
|
+
const origin = `http://127.0.0.1:${port}`;
|
|
53
|
+
if (await requestLocal(new URL("/_stim/metro-warmup", origin), platform, false) !== "ready") return;
|
|
54
|
+
const url = new URL("/index.bundle", origin);
|
|
55
|
+
url.search = new URLSearchParams({
|
|
56
|
+
platform,
|
|
57
|
+
dev: "true",
|
|
58
|
+
lazy: "true",
|
|
59
|
+
minify: "false",
|
|
60
|
+
...platform === "ios" ? { inlineSourceMap: "false" } : {},
|
|
61
|
+
modulesOnly: "false",
|
|
62
|
+
runModule: "true",
|
|
63
|
+
excludeSource: "true",
|
|
64
|
+
sourcePaths: "url-server",
|
|
65
|
+
...appId ? { app: appId } : {}
|
|
66
|
+
}).toString();
|
|
67
|
+
if (!bundleUrl && isExpo) {
|
|
68
|
+
const body = await requestLocal(new URL(origin), platform, false);
|
|
69
|
+
if (body === null) return;
|
|
70
|
+
const manifest = JSON.parse(body);
|
|
71
|
+
bundleUrl = manifest.launchAsset?.url ?? manifest.bundleUrl;
|
|
72
|
+
if (typeof bundleUrl !== "string") return;
|
|
73
|
+
}
|
|
74
|
+
if (bundleUrl) {
|
|
75
|
+
const bundle = new URL(bundleUrl, origin);
|
|
76
|
+
url.pathname = bundle.pathname;
|
|
77
|
+
url.search = bundle.search;
|
|
78
|
+
}
|
|
79
|
+
await requestLocal(url, platform, true);
|
|
80
|
+
} catch {}
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/engine/android-avd-setup.ts
|
|
84
|
+
var AvdBootError = class extends Error {
|
|
85
|
+
remedy;
|
|
86
|
+
constructor(message, remedy, cause) {
|
|
87
|
+
super(message, { cause });
|
|
88
|
+
this.remedy = remedy;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
var AvdRecoveryError = class extends AvdBootError {};
|
|
92
|
+
function findOtherOwner(avdName, projectPath, selectedSlot = "default") {
|
|
93
|
+
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;
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
async function prepareOwnedAvd({ projectPath, slot, label, previousAvdName, systemImage, deviceProfile, configuration, configure, teardown = teardownOwnedAvd }) {
|
|
97
|
+
const { claim, reservation, previous, avdName } = withConfigLock(() => {
|
|
98
|
+
const previous = deviceSlotPlatforms(loadConfig()?.projects?.[projectPath], slot)?.android;
|
|
99
|
+
if (previous?.avdName && previous.avdName !== previousAvdName) throw new Error(`Another Stim run assigned AVD ${previous.avdName} to this workspace. Retry to use it.`);
|
|
100
|
+
if (readParked("android").some((entry) => entry.name === ownedAvdName(label)) || findOtherOwner(ownedAvdName(label), projectPath, slot)) label = `${label}-${randomUUID().slice(0, 8)}`;
|
|
101
|
+
const avdName = ownedAvdName(label);
|
|
102
|
+
const claim = acquireAvdClaim(avdName);
|
|
103
|
+
const reservation = {
|
|
104
|
+
avdName,
|
|
105
|
+
owned: true,
|
|
106
|
+
deviceName: avdName,
|
|
107
|
+
setupIncomplete: true,
|
|
108
|
+
poolConfiguration: configuration
|
|
109
|
+
};
|
|
110
|
+
try {
|
|
111
|
+
setDevice(projectPath, "android", reservation, slot);
|
|
112
|
+
return {
|
|
113
|
+
claim,
|
|
114
|
+
reservation,
|
|
115
|
+
previous,
|
|
116
|
+
avdName
|
|
117
|
+
};
|
|
118
|
+
} catch (error) {
|
|
119
|
+
releaseClaim(claim);
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
const currentRecord = () => deviceSlotPlatforms(loadConfig()?.projects?.[projectPath], slot)?.android;
|
|
124
|
+
const assertReservation = () => {
|
|
125
|
+
if (readParked("android").some((entry) => entry.name === avdName)) throw new Error(`AVD ${avdName} was parked by another Stim run. Retry to adopt it safely.`);
|
|
126
|
+
const owner = findOtherOwner(avdName, projectPath, slot);
|
|
127
|
+
if (owner) throw new Error(`AVD ${avdName} already exists and is owned by another project (${owner}). Retry to allocate a distinct owned emulator.`);
|
|
128
|
+
const current = currentRecord();
|
|
129
|
+
if (!isDeepStrictEqual(current, reservation)) {
|
|
130
|
+
const state = current?.setupIncomplete ? "has incomplete setup" : "was registered or removed";
|
|
131
|
+
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.`);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
let prepared;
|
|
135
|
+
let child;
|
|
136
|
+
const creationRunning = () => child?.pid !== void 0 && processGroupAlive(child.pid);
|
|
137
|
+
let configurationFailure;
|
|
138
|
+
try {
|
|
139
|
+
try {
|
|
140
|
+
const creation = await createOwnedAvd(label, {
|
|
141
|
+
systemImage,
|
|
142
|
+
deviceProfile,
|
|
143
|
+
spawn: (...args) => {
|
|
144
|
+
markClaimChildPending(claim);
|
|
145
|
+
child = getExecutor().spawn(...args);
|
|
146
|
+
if (child.pid !== void 0) {
|
|
147
|
+
const captured = captureProcessIdentity(child.pid);
|
|
148
|
+
if (captured.ok) try {
|
|
149
|
+
setClaimChild(claim, {
|
|
150
|
+
pid: child.pid,
|
|
151
|
+
processToken: captured.token
|
|
152
|
+
});
|
|
153
|
+
} catch {}
|
|
154
|
+
}
|
|
155
|
+
return child;
|
|
156
|
+
}
|
|
157
|
+
}).then((result) => ({ result }), (error) => ({ error }));
|
|
158
|
+
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)}`);
|
|
159
|
+
clearClaimChild(claim);
|
|
160
|
+
if ("error" in creation) throw creation.error;
|
|
161
|
+
prepared = {
|
|
162
|
+
...creation.result,
|
|
163
|
+
created: true
|
|
164
|
+
};
|
|
165
|
+
} catch (error) {
|
|
166
|
+
const message = String(error?.message || error);
|
|
167
|
+
if (!message.includes("already exists")) throw error;
|
|
168
|
+
try {
|
|
169
|
+
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);
|
|
170
|
+
withConfigLock(assertReservation);
|
|
171
|
+
const resolved = resolveOwnedAvdSerial(avdName);
|
|
172
|
+
if (resolved.missing || resolved.notOwned) throw new Error(`AVD ${avdName} could not be verified for recovery. Retry after checking its registration.`, { cause: error });
|
|
173
|
+
if (!resolved.serial) assertOwnedAvdStopped(avdName);
|
|
174
|
+
prepared = {
|
|
175
|
+
avdName,
|
|
176
|
+
created: false,
|
|
177
|
+
systemImage: ownedAvdSystemImage(avdName),
|
|
178
|
+
deviceProfile: ownedAvdDeviceProfile(avdName),
|
|
179
|
+
...resolved.serial ? {
|
|
180
|
+
serial: resolved.serial,
|
|
181
|
+
consolePort: Number(resolved.serial.replace(/^emulator-/, ""))
|
|
182
|
+
} : {}
|
|
183
|
+
};
|
|
184
|
+
} catch (recoveryError) {
|
|
185
|
+
withConfigLock(() => {
|
|
186
|
+
if (!isDeepStrictEqual(currentRecord(), reservation)) return;
|
|
187
|
+
if (previous) setDevice(projectPath, "android", previous, slot);
|
|
188
|
+
else clearDevice(projectPath, "android", slot, avdName);
|
|
189
|
+
});
|
|
190
|
+
if (recoveryError instanceof AvdRecoveryError) throw recoveryError;
|
|
191
|
+
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);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (prepared.created) try {
|
|
195
|
+
configure(avdName);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
configurationFailure = { error };
|
|
198
|
+
}
|
|
199
|
+
if (!configurationFailure) withConfigLock(() => {
|
|
200
|
+
assertReservation();
|
|
201
|
+
setDevice(projectPath, "android", {
|
|
202
|
+
avdName,
|
|
203
|
+
owned: true,
|
|
204
|
+
deviceName: avdName,
|
|
205
|
+
...prepared.created ? {
|
|
206
|
+
poolConfiguration: configuration,
|
|
207
|
+
bootPending: true
|
|
208
|
+
} : {},
|
|
209
|
+
...prepared.consolePort ? { consolePort: prepared.consolePort } : {}
|
|
210
|
+
}, slot);
|
|
211
|
+
});
|
|
212
|
+
} finally {
|
|
213
|
+
if (!creationRunning()) releaseClaim(claim);
|
|
214
|
+
else {
|
|
215
|
+
child?.stdout?.destroy();
|
|
216
|
+
child?.stderr?.destroy();
|
|
217
|
+
child?.unref();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (configurationFailure) {
|
|
221
|
+
const { error } = configurationFailure;
|
|
222
|
+
const cleanup = teardown(avdName, {
|
|
223
|
+
del: true,
|
|
224
|
+
owner: {
|
|
225
|
+
projectPath,
|
|
226
|
+
slot,
|
|
227
|
+
expectedRecord: reservation
|
|
228
|
+
},
|
|
229
|
+
onRemoved: () => {
|
|
230
|
+
clearDevice(projectPath, "android", slot, avdName);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
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\`.` : "";
|
|
234
|
+
throw new Error(`Created owned AVD ${avdName}, but could not configure its AVD settings: ${String(error?.message || error)}${orphan}`, { cause: error });
|
|
235
|
+
}
|
|
236
|
+
return prepared;
|
|
237
|
+
}
|
|
238
|
+
//#endregion
|
|
239
|
+
//#region src/engine/device-android.ts
|
|
240
|
+
const ANDROID_FRESH_BOOT_TIMEOUT_MS = process.platform === "win32" ? 12e5 : 12e4;
|
|
241
|
+
const ANDROID_BOOT_TIMEOUT_MS = process.platform === "win32" ? 12e5 : 24e4;
|
|
242
|
+
async function ensureOwnedAndroidDevice({ record, projectPath, slot, settingsRoot, label, settings, flags, note, out, logFile, alive, configureAvd, teardownAvd }) {
|
|
243
|
+
const projectAvdConfig = androidAvdConfigSetting(settings, settingsRoot);
|
|
244
|
+
const avdConfig = {
|
|
245
|
+
...OWNED_AVD_CONFIG_DEFAULTS,
|
|
246
|
+
...projectAvdConfig
|
|
247
|
+
};
|
|
248
|
+
const requestedProfile = flags.deviceProfile || settings.android?.deviceProfile || null;
|
|
249
|
+
let replacedProfile = null;
|
|
250
|
+
if (record?.setupIncomplete && record.avdName) {
|
|
251
|
+
const avdName = record.avdName;
|
|
252
|
+
const cleanup = teardownAvd(avdName, {
|
|
253
|
+
del: true,
|
|
254
|
+
owner: {
|
|
255
|
+
projectPath,
|
|
256
|
+
slot,
|
|
257
|
+
expectedRecord: record
|
|
258
|
+
},
|
|
259
|
+
onRemoved: () => {
|
|
260
|
+
clearDevice(projectPath, "android", slot, avdName);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
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.");
|
|
264
|
+
record = null;
|
|
265
|
+
}
|
|
266
|
+
const requestedImage = flags.systemImageFlag || null;
|
|
267
|
+
const currentImage = requestedImage && record?.owned && record.avdName ? ownedAvdSystemImage(record.avdName) : null;
|
|
268
|
+
if (record?.avdName && requestedImage && currentImage && currentImage !== requestedImage) {
|
|
269
|
+
const avdName = record.avdName;
|
|
270
|
+
if (!record.bootPending) throw new AvdRecoveryError(`this project's emulator ${avdName} uses system image ${currentImage}, but ${requestedImage} was requested. --system-image replaces only an emulator that never finished a boot, and this one has.`, "Run `stim worktree remove` (or `stim gc --delete`) to reap the current emulator, then `stim android` again to create the requested one, or pass `--slot <name>` to create it beside the current one.");
|
|
271
|
+
replacedProfile = ownedAvdDeviceProfile(avdName);
|
|
272
|
+
const cleanup = teardownAvd(avdName, {
|
|
273
|
+
del: true,
|
|
274
|
+
owner: {
|
|
275
|
+
projectPath,
|
|
276
|
+
slot,
|
|
277
|
+
expectedRecord: record
|
|
278
|
+
},
|
|
279
|
+
onRemoved: () => {
|
|
280
|
+
clearDevice(projectPath, "android", slot, avdName);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
if (cleanup.status === "failed" || cleanup.status === "skipped") throw new AvdRecoveryError(`Owned AVD ${avdName} never finished a boot, but could not be deleted to create one on ${requestedImage} (${cleanup.reason || cleanup.status}). Stim kept the device record.`, "Fix the cause, then retry, or pass `--slot <name>` to create the requested emulator beside it.");
|
|
284
|
+
out(phaseLine("device", `deleted ${avdName}, which never finished a boot, to create one on ${requestedImage}`));
|
|
285
|
+
record = null;
|
|
286
|
+
}
|
|
287
|
+
const deviceProfile = requestedProfile ?? replacedProfile ?? DEFAULT_AVD_DEVICE_PROFILE;
|
|
288
|
+
const configuration = avdPoolConfiguration(androidDataPartitionSizeGbSetting(settings), avdConfig, deviceProfile);
|
|
289
|
+
const configurationBeforeDefaults = avdPoolConfiguration(androidDataPartitionSizeGbSetting(settings), projectAvdConfig, deviceProfile);
|
|
290
|
+
if (record?.avdName) {
|
|
291
|
+
if (record.owned) {
|
|
292
|
+
const resolved = resolveOwnedAvdSerial(record.avdName);
|
|
293
|
+
const currentProfile = resolved.notOwned || resolved.missing ? null : ownedAvdDeviceProfile(record.avdName);
|
|
294
|
+
if (requestedProfile && currentProfile && currentProfile !== requestedProfile) throw new AvdRecoveryError(`this project's emulator uses device profile ${currentProfile}, but ${requestedProfile} was requested. Stim will not silently boot a different model.`, "Run `stim worktree remove` (or `stim gc --delete`) to reap the current emulator, then `stim android` again to create the requested one, or pass `--slot <name>` to create it beside the current one.");
|
|
295
|
+
if (resolved.notOwned) note(chalk.yellow(`Note: recorded AVD ${record.avdName} is not Stim-owned -- creating a fresh owned AVD instead of reusing it.`));
|
|
296
|
+
else if (resolved.serial) {
|
|
297
|
+
const consolePort = Number(resolved.serial.replace(/^emulator-/, ""));
|
|
298
|
+
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}`));
|
|
299
|
+
const updated = {
|
|
300
|
+
...record,
|
|
301
|
+
avdName: record.avdName,
|
|
302
|
+
consolePort,
|
|
303
|
+
owned: true,
|
|
304
|
+
deviceName: record.deviceName ?? record.avdName
|
|
305
|
+
};
|
|
306
|
+
setDevice(projectPath, "android", updated, slot);
|
|
307
|
+
return {
|
|
308
|
+
...updated,
|
|
309
|
+
systemImage: ownedAvdSystemImage(record.avdName),
|
|
310
|
+
deviceProfile: currentProfile
|
|
311
|
+
};
|
|
312
|
+
} else if (!resolved.missing) {
|
|
313
|
+
out(chalk.dim(`Recorded port for owned AVD ${record.avdName} is not currently ours; booting it on a freshly allocated port...`));
|
|
314
|
+
return {
|
|
315
|
+
...await bootOwnedAvdOnFreshPort({
|
|
316
|
+
deviceSlotWait: flags.deviceSlotWait,
|
|
317
|
+
avdName: record.avdName,
|
|
318
|
+
metadata: record,
|
|
319
|
+
projectPath,
|
|
320
|
+
slot,
|
|
321
|
+
deviceName: record.deviceName,
|
|
322
|
+
out,
|
|
323
|
+
logFile,
|
|
324
|
+
alive
|
|
325
|
+
}),
|
|
326
|
+
systemImage: ownedAvdSystemImage(record.avdName),
|
|
327
|
+
deviceProfile: currentProfile
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
} else if (listAvds().includes(record.avdName)) {
|
|
331
|
+
if (!listAdbDevices().emulators.some((e) => e.consolePort === record.consolePort)) {
|
|
332
|
+
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.`));
|
|
333
|
+
note(chalk.dim("Boot it yourself, or run `stim gc --delete` to clear the assignment so Stim can create an owned AVD."));
|
|
334
|
+
}
|
|
335
|
+
return record;
|
|
336
|
+
}
|
|
337
|
+
} else if (record?.serial) {
|
|
338
|
+
note(chalk.yellow(`Note: this project has a stored assignment to physical device ${record.serial}, which Stim no longer keeps.`));
|
|
339
|
+
note(chalk.dim("Creating an owned emulator instead. Pass `--device` to build for a connected device."));
|
|
340
|
+
}
|
|
341
|
+
if (parkedMaxSetting("android").max > 0) {
|
|
342
|
+
const systemImage = pickDefaultSystemImage(listInstalledSystemImages(), { systemImage: flags.systemImage || settings.android?.systemImage })?.pkg;
|
|
343
|
+
const candidates = readParked("android").filter((entry) => entry.systemImage === systemImage && (entry.configuration === configuration || entry.configuration === configurationBeforeDefaults)).toSorted((a, b) => a.parkedAt.localeCompare(b.parkedAt));
|
|
344
|
+
for (const parked of candidates) {
|
|
345
|
+
if (isLegacyDeletionClaim(parked.deletionClaim)) continue;
|
|
346
|
+
const resolved = resolveOwnedAvdSerial(parked.name);
|
|
347
|
+
if (resolved.missing) {
|
|
348
|
+
const result = teardownParkedAvd(parked.name);
|
|
349
|
+
if (result.status === "failed") out(phaseLine("device", `kept ${parked.name}: ${result.reason}`));
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (resolved.notOwned || resolved.serial) continue;
|
|
353
|
+
try {
|
|
354
|
+
assertOwnedAvdStopped(parked.name);
|
|
355
|
+
} catch (error) {
|
|
356
|
+
out(phaseLine("device", `kept parked ${parked.name}: ${String(error?.message || error)}`));
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (ownedAvdSystemImage(parked.name) !== systemImage || !ownedAvdMatchesConfiguration(parked.name, parked.configuration)) {
|
|
360
|
+
const result = teardownParkedAvd(parked.name);
|
|
361
|
+
if (result.status === "failed") out(phaseLine("device", `kept ${parked.name}: ${result.reason}`));
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
const adopted = {
|
|
365
|
+
avdName: parked.name,
|
|
366
|
+
deviceName: parked.name,
|
|
367
|
+
owned: true,
|
|
368
|
+
poolConfiguration: parked.configuration,
|
|
369
|
+
adoptionPending: true
|
|
370
|
+
};
|
|
371
|
+
if (!adoptParked({
|
|
372
|
+
platform: "android",
|
|
373
|
+
projectPath,
|
|
374
|
+
slot,
|
|
375
|
+
udid: parked.udid,
|
|
376
|
+
device: adopted
|
|
377
|
+
})) continue;
|
|
378
|
+
if (parked.configuration !== configuration) try {
|
|
379
|
+
configureAvd(parked.name, {
|
|
380
|
+
dataPartitionSizeGb: androidDataPartitionSizeGbSetting(settings),
|
|
381
|
+
avdConfig
|
|
382
|
+
});
|
|
383
|
+
adopted.poolConfiguration = configuration;
|
|
384
|
+
} catch (error) {
|
|
385
|
+
out(phaseLine("device", `${parked.name} keeps its old AVD settings: ${String(error?.message || error)}`));
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
...await bootOwnedAvdOnFreshPort({
|
|
389
|
+
deviceSlotWait: flags.deviceSlotWait,
|
|
390
|
+
avdName: parked.name,
|
|
391
|
+
projectPath,
|
|
392
|
+
slot,
|
|
393
|
+
metadata: adopted,
|
|
394
|
+
out,
|
|
395
|
+
logFile,
|
|
396
|
+
alive
|
|
397
|
+
}),
|
|
398
|
+
adopted: true,
|
|
399
|
+
systemImage,
|
|
400
|
+
deviceProfile
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const created = await prepareOwnedAvd({
|
|
405
|
+
projectPath,
|
|
406
|
+
slot,
|
|
407
|
+
label,
|
|
408
|
+
previousAvdName: record?.avdName,
|
|
409
|
+
systemImage: flags.systemImage || settings.android?.systemImage || void 0,
|
|
410
|
+
deviceProfile,
|
|
411
|
+
configuration,
|
|
412
|
+
configure: (avdName) => configureAvd(avdName, {
|
|
413
|
+
dataPartitionSizeGb: androidDataPartitionSizeGbSetting(settings),
|
|
414
|
+
avdConfig
|
|
415
|
+
}),
|
|
416
|
+
teardown: teardownAvd
|
|
417
|
+
});
|
|
418
|
+
if (!created.created) {
|
|
419
|
+
out(chalk.dim(phaseLine("device", `recovered ${created.avdName} (unrecorded from a prior run)`)));
|
|
420
|
+
if (created.serial) return {
|
|
421
|
+
...created,
|
|
422
|
+
owned: true,
|
|
423
|
+
deviceName: created.avdName
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
...await bootOwnedAvdOnFreshPort({
|
|
428
|
+
deviceSlotWait: flags.deviceSlotWait,
|
|
429
|
+
avdName: created.avdName,
|
|
430
|
+
metadata: created.created ? {
|
|
431
|
+
poolConfiguration: configuration,
|
|
432
|
+
bootPending: true
|
|
433
|
+
} : void 0,
|
|
434
|
+
projectPath,
|
|
435
|
+
slot,
|
|
436
|
+
deviceName: created.avdName,
|
|
437
|
+
out,
|
|
438
|
+
logFile,
|
|
439
|
+
alive
|
|
440
|
+
}),
|
|
441
|
+
created: created.created,
|
|
442
|
+
systemImage: created.systemImage,
|
|
443
|
+
deviceProfile: created.deviceProfile
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
function claimAndroidConsolePort({ projectPath, slot, avdName, deviceName, livePorts = [], metadata }, { lock = withConfigLock, recordedPorts = () => allConsolePortsAndSerials().androidConsolePorts, record = setDevice } = {}) {
|
|
447
|
+
return lock(() => {
|
|
448
|
+
const consolePort = nextConsolePort([...recordedPorts(), ...livePorts]);
|
|
449
|
+
const claim = {
|
|
450
|
+
...metadata?.poolConfiguration ? { poolConfiguration: metadata.poolConfiguration } : {},
|
|
451
|
+
...metadata?.adoptionPending ? { adoptionPending: true } : {},
|
|
452
|
+
...metadata?.bootPending ? { bootPending: true } : {},
|
|
453
|
+
avdName,
|
|
454
|
+
consolePort,
|
|
455
|
+
owned: true,
|
|
456
|
+
deviceName: deviceName ?? avdName
|
|
457
|
+
};
|
|
458
|
+
record(projectPath, "android", claim, slot);
|
|
459
|
+
return claim;
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
function liveAndroidConsolePorts() {
|
|
463
|
+
const adbLive = listAdbDevices();
|
|
464
|
+
return [...adbLive.emulators.map((e) => e.consolePort), ...adbLive.unhealthy.map((u) => u.consolePort).filter((p) => p != null)];
|
|
465
|
+
}
|
|
466
|
+
async function bootOwnedAvdOnFreshPort({ avdName, deviceSlotWait, metadata, projectPath, slot, deviceName, out, logFile = null, alive = pidExists }) {
|
|
467
|
+
return withDeviceBootAdmission({
|
|
468
|
+
platform: "android",
|
|
469
|
+
key: avdName
|
|
470
|
+
}, async () => {
|
|
471
|
+
const claim = claimAndroidConsolePort({
|
|
472
|
+
projectPath,
|
|
473
|
+
slot,
|
|
474
|
+
avdName,
|
|
475
|
+
deviceName,
|
|
476
|
+
livePorts: liveAndroidConsolePorts(),
|
|
477
|
+
metadata
|
|
478
|
+
});
|
|
479
|
+
const serial = `emulator-${claim.consolePort}`;
|
|
480
|
+
try {
|
|
481
|
+
reportAndroidMemoryPressure(out);
|
|
482
|
+
const pid = bootAndroidEmulator(avdName, claim.consolePort, { logFile });
|
|
483
|
+
out(chalk.dim(phaseLine("device", `waiting for ${serial} to finish booting`)));
|
|
484
|
+
const result = await waitForAndroidBoot({
|
|
485
|
+
serial,
|
|
486
|
+
timeoutMs: ANDROID_FRESH_BOOT_TIMEOUT_MS,
|
|
487
|
+
pid,
|
|
488
|
+
alive,
|
|
489
|
+
out
|
|
490
|
+
});
|
|
491
|
+
if (result.failed) throw new AvdBootError(result.reason, result.remedy);
|
|
492
|
+
const running = getAvdNameForSerial(serial);
|
|
493
|
+
if (running && running !== avdName) throw new Error(`${serial} is running AVD ${running}, not this workspace's owned AVD ${avdName}; refusing to use it.`);
|
|
494
|
+
clearAndroidBootPending(projectPath, avdName, slot);
|
|
495
|
+
const { bootPending: _booted, ...booted } = claim;
|
|
496
|
+
return {
|
|
497
|
+
...booted,
|
|
498
|
+
serial,
|
|
499
|
+
setup: true
|
|
500
|
+
};
|
|
501
|
+
} catch (error) {
|
|
502
|
+
releaseAndroidConsolePort(projectPath, claim.consolePort, slot);
|
|
503
|
+
throw error;
|
|
504
|
+
}
|
|
505
|
+
}, {
|
|
506
|
+
...deviceSlotWait,
|
|
507
|
+
root: projectPath,
|
|
508
|
+
out
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
function reportAndroidMemoryPressure(out) {
|
|
512
|
+
const pressure = readHostMemoryPressure();
|
|
513
|
+
if (pressure === "warning" || pressure === "critical") {
|
|
514
|
+
const named = memoryCulpritAdvice(memoryCulpritsUnder(pressure));
|
|
515
|
+
out(chalk.dim(phaseLine("memory", `macOS reports ${pressure} host memory pressure; emulator boot may be slow.${named ? ` ${named}` : ""} Free memory with \`stim stop\` only in workspaces you own; ask before closing other apps or devices.`)));
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
function androidBootRemedy(pressure) {
|
|
519
|
+
let count = null;
|
|
520
|
+
try {
|
|
521
|
+
const live = countLiveOwnedDevices({
|
|
522
|
+
sims: () => process.platform === "darwin" ? listAllIosSims({ timeoutMs: 2e3 }) : [],
|
|
523
|
+
adb: () => ({
|
|
524
|
+
...listAdbDevices({ timeoutMs: 2e3 }),
|
|
525
|
+
unhealthy: []
|
|
526
|
+
}),
|
|
527
|
+
booting: []
|
|
528
|
+
});
|
|
529
|
+
count = typeof live === "number" ? live : null;
|
|
530
|
+
} catch {}
|
|
531
|
+
const named = memoryCulpritAdvice(memoryCulpritsUnder(pressure));
|
|
532
|
+
const observation = pressure === "warning" || pressure === "critical" ? `macOS reports ${pressure} host memory pressure. ${named ? `${named} ` : ""}` : "";
|
|
533
|
+
const devices = count ? `${count} Stim-owned ${count === 1 ? "device is" : "devices are"} running. ` : "";
|
|
534
|
+
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.`;
|
|
535
|
+
return "Inspect the emulator log and run `stim doctor` to check JAVA_HOME, ANDROID_HOME, and installed system images, then run `stim android` again.";
|
|
536
|
+
}
|
|
537
|
+
async function waitForAndroidBoot({ serial, timeoutMs, pid = null, alive = pidExists, out }) {
|
|
538
|
+
const aborted = () => pid !== null && !alive(pid);
|
|
539
|
+
const wait = (windowMs) => waitForBoot(serial, windowMs, {
|
|
540
|
+
aborted,
|
|
541
|
+
commandTimeoutMs: 5e3
|
|
542
|
+
});
|
|
543
|
+
let result = await wait(timeoutMs);
|
|
544
|
+
if (result.ok) return {
|
|
545
|
+
ok: true,
|
|
546
|
+
serial
|
|
547
|
+
};
|
|
548
|
+
let pressure = readHostMemoryPressure();
|
|
549
|
+
let elapsedMs = timeoutMs;
|
|
550
|
+
if (!result.exited && pid !== null && !aborted() && (pressure === "warning" || pressure === "critical")) {
|
|
551
|
+
const extensionMs = 24e4;
|
|
552
|
+
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).`)));
|
|
553
|
+
result = await wait(extensionMs);
|
|
554
|
+
if (result.ok) return {
|
|
555
|
+
ok: true,
|
|
556
|
+
serial
|
|
557
|
+
};
|
|
558
|
+
elapsedMs += extensionMs;
|
|
559
|
+
pressure = readHostMemoryPressure();
|
|
560
|
+
}
|
|
561
|
+
const exited = result.exited || aborted();
|
|
562
|
+
return {
|
|
563
|
+
failed: true,
|
|
564
|
+
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)}`,
|
|
565
|
+
remedy: exited ? "Inspect the emulator log for the process exit, fix what it reports, then run `stim android` again." : androidBootRemedy(pressure)
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
async function ensureAndroidBooted({ device, deviceSlotWait, projectPath, slot, timeoutMs, out, logFile = null, alive = pidExists }) {
|
|
569
|
+
if (!device?.avdName || !projectPath) return {
|
|
570
|
+
failed: true,
|
|
571
|
+
reason: "No owned Android emulator is recorded for this project."
|
|
572
|
+
};
|
|
573
|
+
let resolved;
|
|
574
|
+
try {
|
|
575
|
+
resolved = resolveOwnedAvdSerial(device.avdName);
|
|
576
|
+
} catch (e) {
|
|
577
|
+
return {
|
|
578
|
+
failed: true,
|
|
579
|
+
reason: `Could not list AVDs: ${e?.message || e}`
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
if (resolved.missing) return {
|
|
583
|
+
failed: true,
|
|
584
|
+
reason: `AVD ${device.avdName} no longer exists. Run \`stim android\` again to create a fresh owned AVD.`
|
|
585
|
+
};
|
|
586
|
+
if (resolved.notOwned) return {
|
|
587
|
+
failed: true,
|
|
588
|
+
reason: `AVD ${device.avdName} is not Stim-owned; refusing to boot it.`
|
|
589
|
+
};
|
|
590
|
+
const avdName = device.avdName;
|
|
591
|
+
const booted = (result) => {
|
|
592
|
+
if (!result.failed) clearAndroidBootPending(projectPath, avdName, slot);
|
|
593
|
+
return result;
|
|
594
|
+
};
|
|
595
|
+
if (resolved.serial) return booted(await waitForAndroidBoot({
|
|
596
|
+
serial: resolved.serial,
|
|
597
|
+
timeoutMs,
|
|
598
|
+
out
|
|
599
|
+
}));
|
|
600
|
+
const freshSerial = `emulator-${device.consolePort}`;
|
|
601
|
+
if (device.owned && device.serial === freshSerial) return booted(await waitForAndroidBoot({
|
|
602
|
+
serial: freshSerial,
|
|
603
|
+
timeoutMs,
|
|
604
|
+
out
|
|
605
|
+
}));
|
|
606
|
+
const boot = async () => {
|
|
607
|
+
const claim = claimAndroidConsolePort({
|
|
608
|
+
projectPath,
|
|
609
|
+
slot,
|
|
610
|
+
avdName,
|
|
611
|
+
deviceName: device.deviceName,
|
|
612
|
+
livePorts: liveAndroidConsolePorts(),
|
|
613
|
+
metadata: device
|
|
614
|
+
});
|
|
615
|
+
const serial = `emulator-${claim.consolePort}`;
|
|
616
|
+
reportAndroidMemoryPressure(out);
|
|
617
|
+
out(chalk.dim(phaseLine("device", `booting ${avdName} as ${serial}`)));
|
|
618
|
+
let pid = null;
|
|
619
|
+
try {
|
|
620
|
+
pid = bootAndroidEmulator(avdName, claim.consolePort, { logFile });
|
|
621
|
+
} catch (e) {
|
|
622
|
+
releaseAndroidConsolePort(projectPath, claim.consolePort, slot);
|
|
623
|
+
return {
|
|
624
|
+
failed: true,
|
|
625
|
+
reason: `Could not start emulator for AVD ${avdName}: ${e?.message || e}`
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
const result = await waitForAndroidBoot({
|
|
629
|
+
serial,
|
|
630
|
+
timeoutMs,
|
|
631
|
+
pid,
|
|
632
|
+
alive,
|
|
633
|
+
out
|
|
634
|
+
});
|
|
635
|
+
if (result.failed) releaseAndroidConsolePort(projectPath, claim.consolePort, slot);
|
|
636
|
+
return booted(result);
|
|
637
|
+
};
|
|
638
|
+
try {
|
|
639
|
+
return await withDeviceBootAdmission({
|
|
640
|
+
platform: "android",
|
|
641
|
+
key: avdName
|
|
642
|
+
}, boot, {
|
|
643
|
+
...deviceSlotWait,
|
|
644
|
+
root: projectPath,
|
|
645
|
+
out
|
|
646
|
+
});
|
|
647
|
+
} catch (error) {
|
|
648
|
+
const refusal = error instanceof DeviceAdmissionRefusal ? error : (0, ownership_claim_exports.claimFailure)(error, "stim android");
|
|
649
|
+
if (!refusal) throw error;
|
|
650
|
+
return {
|
|
651
|
+
failed: true,
|
|
652
|
+
code: refusal.code,
|
|
653
|
+
reason: refusal.message,
|
|
654
|
+
remedy: refusal.remedy
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
//#endregion
|
|
659
|
+
//#region src/engine/device.ts
|
|
660
|
+
/**
|
|
661
|
+
* Whether `ensureOwnedDevice` created, adopted or started booting the device, rather than finding it booted.
|
|
662
|
+
* Undefined for a remote device, which those functions do not describe.
|
|
663
|
+
*/
|
|
664
|
+
function didSetUpDevice(device, remote) {
|
|
665
|
+
return remote ? void 0 : Boolean(device.setup || device.booting);
|
|
666
|
+
}
|
|
667
|
+
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, simSnapshot }) {
|
|
668
|
+
clearDeviceIdleShutdown(projectPath, platform, slot);
|
|
669
|
+
if (slot && slot !== "default") label = `${label}-${slot}`;
|
|
670
|
+
const record = deviceSlotPlatforms(project, slot)?.[platform] ?? null;
|
|
671
|
+
if (platform === "ios") return ensureOwnedIosDevice({
|
|
672
|
+
record,
|
|
673
|
+
projectPath,
|
|
674
|
+
slot,
|
|
675
|
+
settingsRoot,
|
|
676
|
+
label,
|
|
677
|
+
settings,
|
|
678
|
+
flags,
|
|
679
|
+
note,
|
|
680
|
+
out,
|
|
681
|
+
reconcileIosSimulator,
|
|
682
|
+
simSnapshot
|
|
683
|
+
});
|
|
684
|
+
return ensureOwnedAndroidDevice({
|
|
685
|
+
record,
|
|
686
|
+
projectPath,
|
|
687
|
+
slot,
|
|
688
|
+
settingsRoot,
|
|
689
|
+
label,
|
|
690
|
+
settings,
|
|
691
|
+
flags,
|
|
692
|
+
note,
|
|
693
|
+
out,
|
|
694
|
+
logFile,
|
|
695
|
+
alive,
|
|
696
|
+
configureAvd,
|
|
697
|
+
teardownAvd
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
const BOOT_POLL_MS = 500;
|
|
701
|
+
async function ensureBooted({ platform, device, simulatorApp, timeoutMs, pollMs = BOOT_POLL_MS, out = () => {}, logFile = null, alive = pidExists, projectPath, slot, deviceSlotWait }) {
|
|
702
|
+
if (platform === "ios") return ensureIosBooted({
|
|
703
|
+
device,
|
|
704
|
+
projectPath,
|
|
705
|
+
deviceSlotWait,
|
|
706
|
+
simulatorApp,
|
|
707
|
+
timeoutMs: timeoutMs ?? 6e5,
|
|
708
|
+
pollMs,
|
|
709
|
+
out
|
|
710
|
+
});
|
|
711
|
+
if (platform === "android") return ensureAndroidBooted({
|
|
712
|
+
device,
|
|
713
|
+
deviceSlotWait,
|
|
714
|
+
projectPath,
|
|
715
|
+
slot,
|
|
716
|
+
timeoutMs: timeoutMs ?? ANDROID_BOOT_TIMEOUT_MS,
|
|
717
|
+
out,
|
|
718
|
+
logFile,
|
|
719
|
+
alive
|
|
720
|
+
});
|
|
721
|
+
return {
|
|
722
|
+
failed: true,
|
|
723
|
+
reason: `Unknown platform "${platform}".`
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
//#endregion
|
|
727
|
+
export { AvdRecoveryError as a, AvdBootError as i, ensureBooted as n, warmMetro as o, ensureOwnedDevice as r, didSetUpDevice as t };
|