stim 1.11.0 → 1.13.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.
Files changed (82) hide show
  1. package/README.md +3 -1
  2. package/dist/{activity-BtG4Va9l.mjs → activity-DfVKZTEB.mjs} +44 -26
  3. package/dist/{android-BA2i_KAV.mjs → android-BHfAu2mf.mjs} +1 -1
  4. package/dist/{android-C1q4l523.mjs → android-CD8pr8Q2.mjs} +112 -54
  5. package/dist/{android-cas-DfJkwCXW.mjs → android-cas-B9Kv9aIy.mjs} +12 -10
  6. package/dist/{android-C4zG2Tuc.mjs → android-qx_31wJo.mjs} +414 -118
  7. package/dist/{app-install-DHut6JnV.mjs → app-install-CDFY2nUL.mjs} +1 -1
  8. package/dist/budget-E5o91NR1.mjs +1739 -0
  9. package/dist/{build-plan-CeAYgKAx.mjs → build-plan-BTa93o_r.mjs} +125 -107
  10. package/dist/{build-progress-CDwogz6R.mjs → build-progress-l7d-X-ZP.mjs} +76 -8
  11. package/dist/{cache-manifest-Pubfp8wV.mjs → cache-manifest-DWPF4RKJ.mjs} +1 -1
  12. package/dist/cache-manifest.mjs +1 -1
  13. package/dist/cdp-Dq_ypAlJ.mjs +115 -0
  14. package/dist/chrome-B5hWgQJS.mjs +61 -0
  15. package/dist/cli.mjs +17 -16
  16. package/dist/collector-run.mjs +15 -9
  17. package/dist/{command-output-D5z2_bcF.mjs → command-output-BIL7rlX7.mjs} +1 -1
  18. package/dist/{config-BEq0Qw_a.mjs → config-0jCcMM0E.mjs} +10 -1
  19. package/dist/created-devices-Cd558h31.mjs +37 -0
  20. package/dist/{dependency-state-r9UesA9a.mjs → dependency-state-DNN0yfx6.mjs} +1 -1
  21. package/dist/{deps-OF7CD5Dx.mjs → deps-2Wo81np9.mjs} +2 -2
  22. package/dist/{device-BwH2iI3O.mjs → device-Cf7vl-so.mjs} +7 -7
  23. package/dist/{device-lease-D83nqeMX.mjs → device-lease-CL9dZf73.mjs} +55 -6
  24. package/dist/{device-pool-DHj7cdPy.mjs → device-pool-MuoiyRcu.mjs} +4 -4
  25. package/dist/{device-remote-BQZSnnmE.mjs → device-remote-DT9ngZiD.mjs} +254 -579
  26. package/dist/{doctor-D8QdMRMf.mjs → doctor-D2iysNNH.mjs} +50 -20
  27. package/dist/{error-diagnostics-UYFSQwRy.mjs → error-diagnostics-BoiOhck8.mjs} +7 -7
  28. package/dist/{gc-COZteyiq.mjs → gc-eA-PRBX8.mjs} +1022 -131
  29. package/dist/{guide-NDvTmMi-.mjs → guide-BfZ0OmiP.mjs} +909 -132
  30. package/dist/{guide-status-CWvti2Lg.mjs → guide-status-BaMYu55M.mjs} +2 -2
  31. package/dist/{idle-NETbeNiQ.mjs → idle-DxLi5OkN.mjs} +43 -10
  32. package/dist/{in-use-DVhlSoyn.mjs → in-use-wYEaKQM9.mjs} +11 -7
  33. package/dist/{ios-CQLKpNfr.mjs → ios-CcfCeZ19.mjs} +103 -58
  34. package/dist/{ios-DArbmwer.mjs → ios-CmEzZiOj.mjs} +31 -22
  35. package/dist/{ios-device-CwDZo0X4.mjs → ios-device-BW-t_Lkv.mjs} +1 -1
  36. package/dist/{launch-verify-CuPDGXdr.mjs → launch-verify-CuJaBpEa.mjs} +33 -20
  37. package/dist/{logs-B44a1UeU.mjs → logs-DcSRbC2t.mjs} +12 -11
  38. package/dist/{merge-state-DDxS-XIZ.mjs → merge-state-ytFQCeh4.mjs} +38 -8
  39. package/dist/{metro-BWLOJ_Wr.mjs → metro-BPeY1YMh.mjs} +5 -5
  40. package/dist/{named-ports-Dc7neAlc.mjs → named-ports-n7tnJsI8.mjs} +78 -6
  41. package/dist/native-runtime-CiiFHt0e.mjs +74 -0
  42. package/dist/{ownership-D2xbLVoz.mjs → ownership-Bz2kETAS.mjs} +2 -2
  43. package/dist/{ownership-BiojdtAp.mjs → ownership-PUP4Dwai.mjs} +175 -217
  44. package/dist/page-BTu7I60K.mjs +29 -0
  45. package/dist/{ports-DwBaRFJs.mjs → ports-C5UutqpJ.mjs} +5 -5
  46. package/dist/{project-BXZzOABt.mjs → project-DGeclhvO.mjs} +55 -87
  47. package/dist/{reload-By9Dwsrt.mjs → reload-C-cV5yy_.mjs} +80 -21
  48. package/dist/remote-cache--aehr4Pr.mjs +526 -0
  49. package/dist/{server-bare-D_RK1RCj.mjs → server-bare-Bq8BpSIo.mjs} +1 -1
  50. package/dist/server-expo-BeYwP_Xy.mjs +2 -0
  51. package/dist/{server-expo-o6H5gcBc.mjs → server-expo-DUCg52-K.mjs} +2 -2
  52. package/dist/{settings-C0ugALSC.mjs → settings-ssVBToDR.mjs} +29 -17
  53. package/dist/{settings-B5oFZEri.mjs → settings-xB0Frkg6.mjs} +13 -4
  54. package/dist/settings.schema.json +74 -2
  55. package/dist/{simslim-CIR3IYJO.mjs → simslim-Bbg9UWrt.mjs} +13 -6
  56. package/dist/{slot-launch-rggO1Esw.mjs → slot-launch-BIWEW-wI.mjs} +4 -4
  57. package/dist/{spawn-claims-BJwXTMPt.mjs → spawn-claims-PT8aQpCa.mjs} +1 -1
  58. package/dist/{start-BSsf2tUs.mjs → start-BZhItkiL.mjs} +110 -18
  59. package/dist/{start-i8zc9VmS.mjs → start-Rw2GD3nV.mjs} +1 -1
  60. package/dist/{state-WGdIbi_d.mjs → state-DwAU2_-D.mjs} +15 -3
  61. package/dist/state-l1pYwOre.mjs +148 -0
  62. package/dist/{stats-DBYJ5bxL.mjs → stats-CaSwmsSZ.mjs} +4 -4
  63. package/dist/{stats-BAgJaLA4.mjs → stats-D8rUTPHd.mjs} +1 -1
  64. package/dist/{status-CitcBRsC.mjs → status-TJlL0HWo.mjs} +649 -116
  65. package/dist/{status-Q-WDEGCB.mjs → status-sszP4XKU.mjs} +130 -28
  66. package/dist/stim-desktop-CXt8-Z3J.mjs +40 -0
  67. package/dist/{stop-DIhrvu0B.mjs → stop-BZ2qMiBP.mjs} +50 -18
  68. package/dist/{stop-rst5mL14.mjs → stop-DskDKNTU.mjs} +2 -2
  69. package/dist/supervisor-run.mjs +11 -11
  70. package/dist/web-run.d.mts +22 -0
  71. package/dist/web-run.mjs +481 -0
  72. package/dist/web-sWXd5iek.mjs +394 -0
  73. package/dist/{worktree-DhxDTK44.mjs → worktree-3cBa-bQt.mjs} +1 -1
  74. package/dist/{worktree-KOjMX3zA.mjs → worktree-D4Do_XiQ.mjs} +229 -21
  75. package/dist/{worktree-BlizKDpZ.mjs → worktree-DeBQSVtR.mjs} +3 -1
  76. package/dist/{xcode-B5SPD--o.mjs → xcode-LM6A8cNZ.mjs} +7 -7
  77. package/package.json +5 -5
  78. package/dist/budget-De3P2rq_.mjs +0 -620
  79. package/dist/native-run-D7yB_AsB.mjs +0 -143
  80. package/dist/server-expo-B-CCUJZj.mjs +0 -2
  81. package/dist/workspace-process-lock-Cx74RSDZ.mjs +0 -57
  82. package/dist/workspaces-COD2xCz-.mjs +0 -1013
@@ -1,16 +1,18 @@
1
- import { g as loadConfig$1, u as getConfigDir } from "./config-BEq0Qw_a.mjs";
1
+ import { A as ownership_claim_exports, B as deviceSlotPlatforms, E as withConfigLock$1, L as assignSlotDevice, O as process_identity_exports, S as saveConfig, U as projectDeviceSlots, W as removeSlotDevice, _ as loadConfig$1, c as ensureConfig, d as getConfigDir, f as getConfigPath$1 } from "./config-0jCcMM0E.mjs";
2
2
  import { t as getExecutor } from "./exec-DK8KaVHK.mjs";
3
- import { P as withDirLock, c as signalProcessTree, i as pidExists } from "./metro-BWLOJ_Wr.mjs";
4
- import { c as androidDataPartitionSizeBytes } from "./settings-B5oFZEri.mjs";
5
- import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
6
- import { existsSync as existsSync$1, lstatSync as lstatSync$1, mkdirSync as mkdirSync$1, openSync as openSync$1, readFileSync as readFileSync$1, readdirSync as readdirSync$1, realpathSync as realpathSync$1, renameSync as renameSync$1, rmSync as rmSync$1, statSync as statSync$1, writeFileSync as writeFileSync$1 } from "fs";
3
+ import { a as pidExists, l as signalProcessTree } from "./metro-BPeY1YMh.mjs";
4
+ import { n as settingDefault, r as stimDesktopInstalled, t as STIM_DESKTOP_OPEN_OPTIONS } from "./stim-desktop-CXt8-Z3J.mjs";
5
+ import { n as readCreatedDevices$1, r as recordCreatedDevice, t as forgetCreatedDevice } from "./created-devices-Cd558h31.mjs";
6
+ import { c as androidDataPartitionSizeBytes } from "./settings-xB0Frkg6.mjs";
7
+ import { existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
7
8
  import { dirname, isAbsolute, join, resolve } from "path";
8
- import { ANDROID_EMULATOR_APPS, createdDevicesFile, createdDevicesLock, readCreatedDevices, readCreatedDevices as readCreatedDevices$1, settingDefinition } from "@stim-cli/core/state";
9
+ import { ANDROID_EMULATOR_APPS, loadConfig, settingDefinition } from "@stim-cli/core/state";
10
+ import { join as join$1 } from "node:path";
9
11
  import { homedir, tmpdir } from "os";
12
+ import { isDeepStrictEqual } from "node:util";
10
13
  //#region src/devices/android-emulator-viewer.ts
11
- function configuredAndroidEmulatorApp() {
12
- const value = loadConfig$1()?.androidEmulatorApp;
13
- if (value === void 0) return "emulator";
14
+ function configuredAndroidEmulatorApp(platform = process.platform) {
15
+ const value = loadConfig$1()?.androidEmulatorApp ?? settingDefault(settingDefinition("androidEmulatorApp"), () => stimDesktopInstalled(platform)).value;
14
16
  if (ANDROID_EMULATOR_APPS.includes(value)) return value;
15
17
  const error = /* @__PURE__ */ new Error("Invalid androidEmulatorApp in machine config. Use \"emulator\" or \"stim-desktop\".");
16
18
  Object.assign(error, { code: "STIM_BAD_ARG" });
@@ -22,66 +24,232 @@ function openEmulatorInStimDesktop(serial) {
22
24
  "-a",
23
25
  "Stim",
24
26
  `stim-desktop://open?serial=${serial}`
25
- ], {
26
- timeoutMs: 5e3,
27
- killSignal: "SIGKILL"
27
+ ], STIM_DESKTOP_OPEN_OPTIONS);
28
+ }
29
+ const POOL_SETTING_REMEDY = "Run `stim guide settings` for the device pool bounds and where it can be set.";
30
+ const MAX_SETTING = {
31
+ ios: {
32
+ key: "iosParkedMax",
33
+ env: "STIM_POOL_IOS_PARKED_MAX"
34
+ },
35
+ android: {
36
+ key: "androidParkedMax",
37
+ env: "STIM_POOL_ANDROID_PARKED_MAX"
38
+ }
39
+ };
40
+ function parseMax(raw, strings) {
41
+ const value = strings && typeof raw === "string" ? /^\d+$/.test(raw.trim()) ? Number(raw.trim()) : NaN : raw;
42
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) return null;
43
+ return value;
44
+ }
45
+ function parkedMaxSetting(platform, { config, env = process.env } = {}) {
46
+ const { key, env: envKey } = MAX_SETTING[platform];
47
+ const fromEnv = env[envKey];
48
+ const explicit = fromEnv !== void 0 && fromEnv !== "";
49
+ const pool = (config === void 0 ? loadConfig$1() : config)?.pool;
50
+ if (!explicit && pool !== void 0 && (pool === null || typeof pool !== "object" || Array.isArray(pool))) return {
51
+ max: 0,
52
+ error: "Invalid pool value. Expected an object with simulator and emulator bounds."
53
+ };
54
+ const fromConfig = pool !== null && typeof pool === "object" && !Array.isArray(pool) ? pool[key] : void 0;
55
+ const raw = explicit ? fromEnv : fromConfig;
56
+ if (raw === void 0) return {
57
+ max: env.STIM_HOME ? 0 : 3,
58
+ error: null
59
+ };
60
+ const parsed = parseMax(raw, explicit);
61
+ if (parsed === null) return {
62
+ max: 0,
63
+ error: `Invalid ${explicit ? envKey : `pool.${key}`} value ${JSON.stringify(raw)}. Expected a whole number of parked ${platform === "ios" ? "simulators" : "emulators"}, 0 or more.`
64
+ };
65
+ return {
66
+ max: explicit ? parsed : env.STIM_HOME ? 0 : parsed,
67
+ error: null
68
+ };
69
+ }
70
+ function isParkedRecord(value, platform) {
71
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
72
+ const record = value;
73
+ return typeof record.udid === "string" && typeof record.name === "string" && record.name.startsWith("stim-") && typeof record.parkedAt === "string" && (platform === "android" ? record.udid === record.name && /^stim-[A-Za-z0-9._-]+$/.test(record.name) && typeof record.systemImage === "string" && typeof record.configuration === "string" : typeof record.deviceTypeIdentifier === "string" && typeof record.runtimeIdentifier === "string" && typeof record.simslimManaged === "boolean" && (record.bundleId === void 0 || typeof record.bundleId === "string") && (record.cacheKey === void 0 || typeof record.cacheKey === "string"));
74
+ }
75
+ function poolBlock(config) {
76
+ const raw = config?.parked;
77
+ const block = raw !== null && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
78
+ const read = (platform) => {
79
+ const list = block[platform];
80
+ return Array.isArray(list) ? list.filter((record) => isParkedRecord(record, platform)) : [];
81
+ };
82
+ return {
83
+ ios: read("ios"),
84
+ android: read("android")
85
+ };
86
+ }
87
+ function readParked(platform, { config } = {}) {
88
+ return poolBlock(config === void 0 ? loadConfig$1() : config)[platform];
89
+ }
90
+ function writeParked(config, platform, records) {
91
+ config.parked = {
92
+ ...poolBlock(config),
93
+ [platform]: records
94
+ };
95
+ }
96
+ function oldestFirst(records) {
97
+ return records.toSorted((a, b) => String(a.parkedAt).localeCompare(String(b.parkedAt)));
98
+ }
99
+ function isLegacyDeletionClaim(value) {
100
+ if (value === void 0) return false;
101
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return true;
102
+ const marker = value;
103
+ return marker.kind !== "ownership-claim" || typeof marker.claimId !== "string" || marker.claimId.length === 0;
104
+ }
105
+ function selectParked(records, { deviceTypeIdentifier, runtimeIdentifier }) {
106
+ return oldestFirst(records.filter((r) => !isLegacyDeletionClaim(r.deletionClaim) && r.deviceTypeIdentifier === deviceTypeIdentifier && r.runtimeIdentifier === runtimeIdentifier));
107
+ }
108
+ function evictOverflow(records, max) {
109
+ if (records.length <= max) return {
110
+ keep: [...records],
111
+ evicted: []
112
+ };
113
+ const ordered = oldestFirst(records);
114
+ const evicted = ordered.slice(0, ordered.length - max);
115
+ const dropped = new Set(evicted.map((r) => r.udid));
116
+ return {
117
+ keep: records.filter((r) => !dropped.has(r.udid)),
118
+ evicted
119
+ };
120
+ }
121
+ function parkSim({ platform, projectPath, slot = "default", record, max }) {
122
+ return withConfigLock$1(() => {
123
+ const cfg = ensureConfig();
124
+ const current = deviceSlotPlatforms(cfg.projects[projectPath], slot)?.[platform];
125
+ const currentId = platform === "ios" ? current?.deviceUdid : current?.avdName;
126
+ if (!current?.owned || currentId !== record.udid) throw new Error(`The ${platform === "ios" ? "simulator" : "emulator"} assignment changed before parking.`);
127
+ const { keep, evicted } = evictOverflow([...readParked(platform, { config: cfg }).filter((r) => r.udid !== record.udid), record], max);
128
+ writeParked(cfg, platform, [...keep, ...evicted]);
129
+ const project = cfg.projects[projectPath];
130
+ if (project) removeSlotDevice(project, platform, slot);
131
+ saveConfig(cfg);
132
+ return evicted;
28
133
  });
29
134
  }
30
- //#endregion
31
- //#region src/devices/created-devices.ts
32
- function update(platform, change) {
33
- const dir = getConfigDir();
34
- withDirLock(createdDevicesLock(), () => {
35
- const current = readCreatedDevices();
36
- const entries = new Set(current[platform]);
37
- if (!change(entries)) return;
38
- const next = {
39
- version: 1,
40
- ios: [...current.ios],
41
- android: [...current.android],
42
- [platform]: [...entries]
43
- };
44
- const file = createdDevicesFile();
45
- const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
46
- writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`);
135
+ function adoptParked({ platform, projectPath, slot = "default", udid, device }) {
136
+ const claim = claimParkedOperation(platform, udid);
137
+ if (!claim) return null;
138
+ try {
139
+ return withConfigLock$1(() => {
140
+ const cfg = ensureConfig();
141
+ const records = readParked(platform, { config: cfg });
142
+ const taken = records.find((r) => r.udid === udid);
143
+ if (!taken || isLegacyDeletionClaim(taken.deletionClaim)) return null;
144
+ if (deviceSlotPlatforms(cfg.projects[projectPath], slot)?.[platform]) return null;
145
+ writeParked(cfg, platform, records.filter((r) => r.udid !== udid));
146
+ const project = cfg.projects[projectPath];
147
+ if (!project) throw new Error(`Project not registered: ${projectPath}`);
148
+ assignSlotDevice(project, platform, device, slot);
149
+ saveConfig(cfg);
150
+ const adopted = { ...taken };
151
+ delete adopted.deletionClaim;
152
+ return adopted;
153
+ });
154
+ } finally {
155
+ (0, ownership_claim_exports.releaseClaim)(claim);
156
+ }
157
+ }
158
+ function claimParkedOperation(platform, udid) {
159
+ const attempt = (0, ownership_claim_exports.tryAcquireClaim)({
160
+ root: join$1(getConfigDir(), "pool-locks", platform, `${encodeURIComponent(udid.toLowerCase())}.lock`),
161
+ mode: "exclusive",
162
+ label: `parked ${platform} device ${udid}`
163
+ });
164
+ if (attempt.pending) (0, ownership_claim_exports.releaseClaim)(attempt.pending);
165
+ return attempt.acquired ?? null;
166
+ }
167
+ function removeParkedAfter(platform, udid, beforeRemove) {
168
+ const claim = claimParkedOperation(platform, udid);
169
+ if (!claim) return null;
170
+ try {
171
+ const claimed = withConfigLock$1(() => {
172
+ const cfg = loadConfig$1();
173
+ if (!cfg) return null;
174
+ const records = readParked(platform, { config: cfg });
175
+ const current = records.find((candidate) => candidate.udid === udid);
176
+ if (!current) return null;
177
+ if (isLegacyDeletionClaim(current.deletionClaim)) throw new Error(`Parked ${platform} device ${udid} has a legacy deletionClaim in ${getConfigPath$1()}. Its PID does not establish whether the original operation finished. Keep the device and pool record; after verifying that neither the old Stim process nor its native child is using it, remove only that record's deletionClaim field and retry.`);
178
+ const record = { ...current };
179
+ delete record.deletionClaim;
180
+ const marked = {
181
+ ...record,
182
+ deletionClaim: {
183
+ kind: "ownership-claim",
184
+ claimId: claim.claimId
185
+ }
186
+ };
187
+ writeParked(cfg, platform, records.map((candidate) => candidate.udid === udid ? marked : candidate));
188
+ saveConfig(cfg);
189
+ return {
190
+ record,
191
+ marked
192
+ };
193
+ });
194
+ if (!claimed) return null;
195
+ let completed = false;
196
+ let removed = null;
47
197
  try {
48
- renameSync(tmp, file);
49
- } catch (error) {
50
- rmSync(tmp, { force: true });
51
- throw error;
198
+ (0, ownership_claim_exports.markClaimChildPending)(claim);
199
+ try {
200
+ beforeRemove(claimed.record);
201
+ } finally {
202
+ (0, ownership_claim_exports.clearClaimChild)(claim);
203
+ }
204
+ completed = true;
205
+ } finally {
206
+ withConfigLock$1(() => {
207
+ const cfg = loadConfig$1();
208
+ if (!cfg) return;
209
+ const records = readParked(platform, { config: cfg });
210
+ const current = records.find((candidate) => candidate.udid === udid);
211
+ if (!isDeepStrictEqual(current, claimed.marked)) return;
212
+ writeParked(cfg, platform, completed ? records.filter((candidate) => candidate.udid !== udid) : records.map((candidate) => candidate.udid === udid ? claimed.record : candidate));
213
+ saveConfig(cfg);
214
+ if (completed) removed = claimed.record;
215
+ });
52
216
  }
53
- }, { ensureParent: () => mkdirSync(dir, { recursive: true }) });
54
- }
55
- function recordCreatedDevice(platform, id) {
56
- update(platform, (entries) => entries.size !== entries.add(id).size);
217
+ return removed;
218
+ } finally {
219
+ (0, ownership_claim_exports.releaseClaim)(claim);
220
+ }
57
221
  }
58
- function forgetCreatedDevice(platform, id) {
59
- update(platform, (entries) => entries.delete(id));
222
+ function dropParked(platform, udid) {
223
+ return removeParkedAfter(platform, udid, () => {}) !== null;
60
224
  }
61
225
  //#endregion
62
226
  //#region src/devices/device-ownership.ts
63
- const OWNED_SIM_NAME = /^stim-[A-Za-z0-9._-]+ \(.* \d+(?:\.\d+)*\)(?: [A-Za-z0-9-]+)?$/;
64
- const OWNED_AVD_NAME = /^stim-[A-Za-z0-9._-]+$/;
65
- const GIB = 1024 ** 3;
66
- function isOwnedSimName(name) {
67
- return OWNED_SIM_NAME.test(name);
68
- }
69
- function isOwnedAvdName(name) {
70
- return OWNED_AVD_NAME.test(name);
71
- }
72
- function avdConfigWrittenByStim(configIni) {
73
- for (const line of String(configIni ?? "").split(/\r?\n/)) {
74
- const match = /^disk\.dataPartition\.size=(\d+)$/.exec(line.trim());
75
- if (match) return Number(match[1]) > 0 && Number(match[1]) % GIB === 0;
227
+ function recordedAsOwned(platform, id) {
228
+ const config = loadConfig();
229
+ if (!config) return false;
230
+ if (platform === "ios" ? readParked("ios", { config }).some((sim) => sim.udid === id) : readParked("android", { config }).some((avd) => avd.name === id)) return true;
231
+ for (const project of Object.values(config.projects ?? {})) {
232
+ let slots;
233
+ try {
234
+ slots = projectDeviceSlots(project);
235
+ } catch {
236
+ continue;
237
+ }
238
+ for (const { platforms } of slots) {
239
+ const record = platforms[platform];
240
+ if (record?.owned && (platform === "ios" ? record.deviceUdid : record.avdName) === id) return true;
241
+ }
76
242
  }
77
243
  return false;
78
244
  }
79
- function isStimOwnedSim(sim, created = readCreatedDevices$1()) {
80
- const name = sim.name ?? "";
81
- return name.startsWith("stim-") && (created.ios.has(sim.udid) || isOwnedSimName(name));
245
+ function isStimOwnedDevice(platform, id) {
246
+ return readCreatedDevices$1()[platform].has(id) || recordedAsOwned(platform, id);
82
247
  }
83
- function isStimOwnedAvd(name, readConfig, created = readCreatedDevices$1()) {
84
- return isOwnedAvdName(name) && (created.android.has(name) || avdConfigWrittenByStim(readConfig()));
248
+ function isStimOwnedSim(sim) {
249
+ return (sim.name ?? "").startsWith("stim-") && isStimOwnedDevice("ios", sim.udid);
250
+ }
251
+ function isStimOwnedAvd(name) {
252
+ return name.startsWith("stim-") && isStimOwnedDevice("android", name);
85
253
  }
86
254
  //#endregion
87
255
  //#region src/devices/android.ts
@@ -92,10 +260,10 @@ const ANDROID_ABIS = /* @__PURE__ */ new Set([
92
260
  "x86_64"
93
261
  ]);
94
262
  const DEFAULT_AVD_DEVICE_PROFILE = String(settingDefinition("android.deviceProfile")?.default);
95
- function androidHome() {
96
- const configured = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
263
+ function androidHome(env = process.env) {
264
+ const configured = env.ANDROID_HOME || env.ANDROID_SDK_ROOT;
97
265
  if (configured) return configured;
98
- if (process.platform === "win32") return join(process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local"), "Android", "Sdk");
266
+ if (process.platform === "win32") return join(env.LOCALAPPDATA || join(homedir(), "AppData", "Local"), "Android", "Sdk");
99
267
  return join(homedir(), "Library", "Android", "sdk");
100
268
  }
101
269
  const SDK_TOOL_LOCATIONS = {
@@ -106,19 +274,26 @@ const SDK_TOOL_LOCATIONS = {
106
274
  "latest",
107
275
  "bin",
108
276
  "avdmanager"
277
+ ],
278
+ sdkmanager: [
279
+ "cmdline-tools",
280
+ "latest",
281
+ "bin",
282
+ "sdkmanager"
109
283
  ]
110
284
  };
111
285
  const WINDOWS_SDK_TOOL_EXTENSIONS = {
112
286
  emulator: ".exe",
113
287
  adb: ".exe",
114
- avdmanager: ".bat"
288
+ avdmanager: ".bat",
289
+ sdkmanager: ".bat"
115
290
  };
116
291
  function androidToolPath(tool, platform = process.platform) {
117
292
  const location = SDK_TOOL_LOCATIONS[tool];
118
293
  const bare = location.at(-1);
119
294
  const name = platform === "win32" ? `${bare}${WINDOWS_SDK_TOOL_EXTENSIONS[tool]}` : bare;
120
295
  const abs = join(androidHome(), ...location.slice(0, -1), name);
121
- return existsSync$1(abs) ? abs : bare;
296
+ return existsSync(abs) ? abs : bare;
122
297
  }
123
298
  const WINDOWS_BUILD_TOOL_EXTENSIONS = { apksigner: ".bat" };
124
299
  function androidBuildToolName(tool, platform = process.platform) {
@@ -140,7 +315,7 @@ function buildToolsMajor(version) {
140
315
  const major = Number(String(version ?? "").split(/[.-]/)[0]);
141
316
  return Number.isFinite(major) ? major : 0;
142
317
  }
143
- function findBuildTool(tools, { home = androidHome(), readDir = readdirSync$1, exists = existsSync$1, platform = process.platform } = {}) {
318
+ function findBuildTool(tools, { home = androidHome(), readDir = readdirSync, exists = existsSync, platform = process.platform } = {}) {
144
319
  const root = join(home, "build-tools");
145
320
  let versions = [];
146
321
  try {
@@ -168,15 +343,23 @@ function androidTool(tool) {
168
343
  const resolved = androidToolPath(tool);
169
344
  return resolved === tool ? tool : `"${resolved}"`;
170
345
  }
171
- function listInstalledSystemImages() {
346
+ function listInstalledSystemImages(onUnreadable) {
172
347
  const root = join(androidHome(), "system-images");
173
348
  const images = [];
174
- if (!existsSync$1(root)) return images;
175
- for (const apiDir of safeList(root)) {
349
+ const list = (dir) => {
350
+ try {
351
+ return readdirSync(dir);
352
+ } catch (error) {
353
+ const code = error.code;
354
+ if (code !== "ENOENT" && code !== "ENOTDIR") onUnreadable?.(error);
355
+ return [];
356
+ }
357
+ };
358
+ for (const apiDir of list(root)) {
176
359
  const m = apiDir.match(/^android-(\d+)$/);
177
360
  if (!m) continue;
178
361
  const apiPath = join(root, apiDir);
179
- for (const tag of safeList(apiPath)) for (const arch of safeList(join(apiPath, tag))) images.push({
362
+ for (const tag of list(apiPath)) for (const arch of list(join(apiPath, tag))) images.push({
180
363
  api: Number(m[1]),
181
364
  tag,
182
365
  arch,
@@ -185,13 +368,6 @@ function listInstalledSystemImages() {
185
368
  }
186
369
  return images;
187
370
  }
188
- function safeList(dir) {
189
- try {
190
- return readdirSync$1(dir);
191
- } catch {
192
- return [];
193
- }
194
- }
195
371
  function pageSizeRank(image) {
196
372
  return /(^|_)ps16k$/.test(String(image?.tag || "")) ? 1 : 0;
197
373
  }
@@ -205,6 +381,18 @@ function pickDefaultSystemImage(images, { systemImage, hostArch } = {}) {
205
381
  if (matching.length === 0) return null;
206
382
  return matching.toSorted((a, b) => pageSizeRank(a) - pageSizeRank(b) || b.api - a.api || (b.tag === "google_apis" ? 1 : 0) - (a.tag === "google_apis" ? 1 : 0))[0] ?? null;
207
383
  }
384
+ const FOLD_FEATURE_PROFILES = /* @__PURE__ */ new Set(["pixel_fold", "resizable"]);
385
+ function profileNeedsFoldFeature(profile) {
386
+ return typeof profile === "string" && FOLD_FEATURE_PROFILES.has(profile);
387
+ }
388
+ function systemImageSupportsFold(pkg, home = androidHome()) {
389
+ try {
390
+ const ini = readFileSync(join(home, ...pkg.split(";"), "advancedFeatures.ini"), "utf8");
391
+ return /^\s*SupportPixelFold\s*=\s*on\s*$/im.test(ini);
392
+ } catch {
393
+ return false;
394
+ }
395
+ }
208
396
  async function createOwnedAvd(label, { systemImage, deviceProfile = DEFAULT_AVD_DEVICE_PROFILE, spawn = (...args) => getExecutor().spawn(...args) } = {}) {
209
397
  const pick = pickDefaultSystemImage(listInstalledSystemImages(), { systemImage });
210
398
  if (!pick) {
@@ -271,7 +459,7 @@ function avdIniValue(configIni, key) {
271
459
  }
272
460
  return null;
273
461
  }
274
- function ownedAvdSystemImage(avdName, { avdDirectory = ownedAvdDirectory, readFile = (path) => readFileSync$1(path, "utf8") } = {}) {
462
+ function ownedAvdSystemImage(avdName, { avdDirectory = ownedAvdDirectory, readFile = (path) => readFileSync(path, "utf8") } = {}) {
275
463
  const directory = avdDirectory(avdName);
276
464
  if (!directory) return null;
277
465
  try {
@@ -298,24 +486,18 @@ function sanitizeAvdLabel(label) {
298
486
  function avdConfigIni(avdName, directory = ownedAvdDirectory(avdName)) {
299
487
  if (!directory) return null;
300
488
  try {
301
- return readFileSync$1(join(directory, "config.ini"), "utf8");
489
+ return readFileSync(join(directory, "config.ini"), "utf8");
302
490
  } catch {
303
491
  return null;
304
492
  }
305
493
  }
306
- function registeredOrOrphanedAvdDirectory(avdName) {
307
- return ownedAvdDirectory(avdName) ?? listOrphanedAvdDirectories(avdName)[0]?.directory ?? null;
308
- }
309
- function isStimOwnedAvdName(avdName, directory) {
310
- return isStimOwnedAvd(avdName, () => avdConfigIni(avdName, directory === void 0 ? registeredOrOrphanedAvdDirectory(avdName) : directory));
311
- }
312
494
  function ownedAvdName(label) {
313
495
  const clean = sanitizeAvdLabel(label);
314
496
  return `stim-${clean.startsWith("stim-") ? clean.slice(5) : clean}`;
315
497
  }
316
498
  const AVDMANAGER_DELETE_TIMEOUT_MS = 12e4;
317
499
  function deleteAvd(avdName) {
318
- if (!isStimOwnedAvdName(avdName)) throw new Error(`Refusing to delete AVD "${avdName}": not a Stim-owned AVD; Stim has no record of creating it.`);
500
+ if (!isStimOwnedAvd(avdName)) throw new Error(`Refusing to delete AVD "${avdName}": not a Stim-owned AVD; Stim has no record of creating it.`);
319
501
  getExecutor().run(`${androidTool("avdmanager")} delete avd -n "${avdName}"`, {
320
502
  timeoutMs: AVDMANAGER_DELETE_TIMEOUT_MS,
321
503
  killSignal: "SIGKILL"
@@ -536,28 +718,45 @@ function avdStorageRoots() {
536
718
  }
537
719
  function avdPathExists(path) {
538
720
  try {
539
- lstatSync$1(path);
721
+ lstatSync(path);
540
722
  return true;
541
723
  } catch (error) {
542
724
  if (error.code === "ENOENT") return false;
543
725
  throw error;
544
726
  }
545
727
  }
728
+ /**
729
+ * True only when the AVD home the emulator uses first exists and no AVD root holds a registration or data
730
+ * directory for `avdName`. A missing first root may be an unmounted volume, so it proves nothing; an
731
+ * unreadable path throws.
732
+ */
733
+ function avdNameAbsent(avdName, roots = avdStorageRoots()) {
734
+ if (!roots[0] || !isDirectoryPath(roots[0])) return false;
735
+ return roots.every((root) => !avdPathExists(join(root, `${avdName}.ini`)) && !avdPathExists(join(root, `${avdName}.avd`)));
736
+ }
737
+ function isDirectoryPath(path) {
738
+ try {
739
+ return statSync(path).isDirectory();
740
+ } catch (error) {
741
+ if (error.code === "ENOENT") return false;
742
+ throw error;
743
+ }
744
+ }
546
745
  function listOrphanedAvdDirectories(avdName) {
547
746
  const roots = avdStorageRoots();
548
747
  const stores = roots.filter(avdPathExists).map((root) => ({
549
748
  root,
550
- canonicalRoot: realpathSync$1(root),
551
- names: readdirSync$1(root)
749
+ canonicalRoot: realpathSync(root),
750
+ names: readdirSync(root)
552
751
  }));
553
752
  const registered = /* @__PURE__ */ new Set();
554
753
  for (const { root, canonicalRoot, names } of stores) for (const name of names.filter((entry) => entry.endsWith(".ini"))) {
555
754
  const path = join(root, name);
556
- const ini = readFileSync$1(path, "utf8");
755
+ const ini = readFileSync(path, "utf8");
557
756
  const targets = /* @__PURE__ */ new Set([...avdIniPaths(root, ini), ...avdIniPaths(canonicalRoot, ini)]);
558
757
  if (!targets.size) throw new Error(`Cannot verify AVD registration at ${path}; its data was kept.`);
559
758
  for (const target of targets) try {
560
- registered.add(realpathSync$1(target));
759
+ registered.add(realpathSync(target));
561
760
  } catch (error) {
562
761
  const code = error.code;
563
762
  if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
@@ -572,8 +771,8 @@ function listOrphanedAvdDirectories(avdName) {
572
771
  if (roots.some((candidate) => avdPathExists(join(candidate, `${name}.ini`)))) continue;
573
772
  const directory = join(canonicalRoot, entry);
574
773
  if (registered.has(directory) || !avdPathExists(directory)) continue;
575
- const stat = lstatSync$1(directory);
576
- if (!stat.isDirectory() || stat.isSymbolicLink() || realpathSync$1(directory) !== directory) throw new Error(`Cannot verify AVD data at ${directory}; it was kept.`);
774
+ const stat = lstatSync(directory);
775
+ if (!stat.isDirectory() || stat.isSymbolicLink() || realpathSync(directory) !== directory) throw new Error(`Cannot verify AVD data at ${directory}; it was kept.`);
577
776
  if (!orphaned.some((candidate) => candidate.directory === directory)) orphaned.push({
578
777
  name,
579
778
  directory,
@@ -584,7 +783,7 @@ function listOrphanedAvdDirectories(avdName) {
584
783
  }
585
784
  return orphaned;
586
785
  }
587
- function ownedAvdDirectory(avdName, { env = process.env, home = homedir(), readFile = (path) => readFileSync$1(path, "utf8"), realpath = realpathSync$1, isDirectory = (path) => statSync$1(path).isDirectory() } = {}) {
786
+ function ownedAvdDirectory(avdName, { env = process.env, home = homedir(), readFile = (path) => readFileSync(path, "utf8"), realpath = realpathSync, isDirectory = (path) => statSync(path).isDirectory() } = {}) {
588
787
  if (!/^stim-[A-Za-z0-9._-]+$/.test(avdName)) return null;
589
788
  const roots = [
590
789
  env.ANDROID_AVD_HOME,
@@ -618,7 +817,7 @@ const AVD_USER_DATA = [
618
817
  "snapshots"
619
818
  ];
620
819
  function wipeAvdUserData(directory) {
621
- for (const name of AVD_USER_DATA) rmSync$1(join(directory, name), {
820
+ for (const name of AVD_USER_DATA) rmSync(join(directory, name), {
622
821
  recursive: true,
623
822
  force: true
624
823
  });
@@ -644,10 +843,10 @@ function withAvdConfigOverrides(contents, overrides) {
644
843
  return updated.join(newline) + (trailingNewline ? newline : "");
645
844
  }
646
845
  let avdConfigWriteSequence = 0;
647
- function configureNewOwnedAvd(avdName, { dataPartitionSizeGb, avdConfig = {} }, { avdDirectory = ownedAvdDirectory, readFile = (path) => readFileSync$1(path, "utf8"), writeFile = (path, contents) => writeFileSync$1(path, contents, {
846
+ function configureNewOwnedAvd(avdName, { dataPartitionSizeGb, avdConfig = {} }, { avdDirectory = ownedAvdDirectory, readFile = (path) => readFileSync(path, "utf8"), writeFile = (path, contents) => writeFileSync(path, contents, {
648
847
  encoding: "utf8",
649
848
  flag: "wx"
650
- }), rename = renameSync$1, remove = (path) => rmSync$1(path, { force: true }) } = {}) {
849
+ }), rename = renameSync, remove = (path) => rmSync(path, { force: true }) } = {}) {
651
850
  const sizeBytes = androidDataPartitionSizeBytes(dataPartitionSizeGb);
652
851
  const directory = avdDirectory(avdName);
653
852
  if (!directory) throw new Error(`Could not resolve the content directory for newly created AVD ${avdName}.`);
@@ -672,6 +871,11 @@ function configureNewOwnedAvd(avdName, { dataPartitionSizeGb, avdConfig = {} },
672
871
  for (const [key, value] of Object.entries(expected)) if (!verified.split(/\r?\n/).includes(`${key}=${value}`)) throw new Error(`Could not verify ${key} in ${configPath}.`);
673
872
  return configPath;
674
873
  }
874
+ /**
875
+ * Values every AVD Stim creates gets unless `android.avdConfig` or `android.avdConfigFile` sets the key. The
876
+ * emulator drops gRPC `sendKey` events on an AVD without a hardware keyboard.
877
+ */
878
+ const OWNED_AVD_CONFIG_DEFAULTS = Object.freeze({ "hw.keyboard": "yes" });
675
879
  function avdPoolConfiguration(dataPartitionSizeGb, avdConfig, deviceProfile = DEFAULT_AVD_DEVICE_PROFILE) {
676
880
  return JSON.stringify(Object.entries({
677
881
  ...avdConfig,
@@ -682,7 +886,7 @@ function avdPoolConfiguration(dataPartitionSizeGb, avdConfig, deviceProfile = DE
682
886
  function ownedAvdMatchesConfiguration(avdName, configuration) {
683
887
  const directory = ownedAvdDirectory(avdName);
684
888
  if (!directory) return false;
685
- const contents = readFileSync$1(join(directory, "config.ini"), "utf8");
889
+ const contents = readFileSync(join(directory, "config.ini"), "utf8");
686
890
  const expected = JSON.parse(configuration);
687
891
  return Array.isArray(expected) && expected.length > 0 && expected.every((entry) => Array.isArray(entry) && entry.length === 2 && entry.every((value) => typeof value === "string") && contents.split(/\r?\n/).some((line) => line.trim() === `${entry[0]}=${entry[1]}`));
688
892
  }
@@ -765,7 +969,7 @@ const CRASH_REPORT_MODE_EMULATOR_VERSION = 37;
765
969
  function emulatorCrashDatabases(env = process.env) {
766
970
  const root = join(env.TMP || env.TEMP || tmpdir(), "AndroidEmulator");
767
971
  try {
768
- return readdirSync$1(root).filter((name) => /^emu-crash-.*\.db$/.test(name)).map((name) => join(root, name));
972
+ return readdirSync(root).filter((name) => /^emu-crash-.*\.db$/.test(name)).map((name) => join(root, name));
769
973
  } catch {
770
974
  return [];
771
975
  }
@@ -779,7 +983,7 @@ function emulatorCrashDatabases(env = process.env) {
779
983
  * removed instead, which has the same effect unless a crash handler is still writing into it.
780
984
  * https://github.com/appandflow/stim/issues/918
781
985
  */
782
- function suppressEmulatorCrashConsent({ platform = process.platform, version = emulatorMajorVersion, databases = emulatorCrashDatabases, remove = (path) => rmSync$1(path, {
986
+ function suppressEmulatorCrashConsent({ platform = process.platform, version = emulatorMajorVersion, databases = emulatorCrashDatabases, remove = (path) => rmSync(path, {
783
987
  recursive: true,
784
988
  force: true
785
989
  }) } = {}) {
@@ -793,7 +997,7 @@ function suppressEmulatorCrashConsent({ platform = process.platform, version = e
793
997
  }
794
998
  function bootAndroidEmulator(avdName, consolePort, { logFile, platform = process.platform } = {}) {
795
999
  const exec = getExecutor();
796
- const app = configuredAndroidEmulatorApp();
1000
+ const app = configuredAndroidEmulatorApp(platform);
797
1001
  const child = exec.spawn(androidToolPath("emulator"), [
798
1002
  "-avd",
799
1003
  avdName,
@@ -817,8 +1021,8 @@ function bootAndroidEmulator(avdName, consolePort, { logFile, platform = process
817
1021
  function emulatorStdio(logFile) {
818
1022
  if (!logFile) return "ignore";
819
1023
  try {
820
- mkdirSync$1(dirname(logFile), { recursive: true });
821
- const fd = openSync$1(logFile, "w");
1024
+ mkdirSync(dirname(logFile), { recursive: true });
1025
+ const fd = openSync(logFile, "w");
822
1026
  return [
823
1027
  "ignore",
824
1028
  fd,
@@ -923,7 +1127,7 @@ function avdProcessLockPaths(avdDirectory, platform) {
923
1127
  }
924
1128
  function readAvdProcessId(path) {
925
1129
  try {
926
- const pid = Number(readFileSync$1(path, "utf8").split("\0")[0]);
1130
+ const pid = Number(readFileSync(path, "utf8").split("\0")[0]);
927
1131
  if (!Number.isSafeInteger(pid) || pid <= 0) throw new Error(`Could not read the emulator PID from process lock ${path}.`);
928
1132
  return pid;
929
1133
  } catch (error) {
@@ -947,9 +1151,37 @@ function resolveAvdProcess(avdName, { platform = process.platform, resolveDirect
947
1151
  processId: null
948
1152
  };
949
1153
  }
950
- function assertOwnedAvdStopped(avdName, { processAlive = pidExists, ...resolveOptions } = {}) {
1154
+ function escapeRegExp(value) {
1155
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1156
+ }
1157
+ function parseAvdEmulatorProcesses(psOutput, avdName) {
1158
+ const command = new RegExp(`(?:^|/)(?:qemu-system-[^\\s/]+|emulator)(?:\\s.*)?\\s(?:-avd\\s+|@)${escapeRegExp(avdName)}(?:\\s|$)`);
1159
+ const pids = [];
1160
+ for (const line of psOutput.split("\n")) {
1161
+ const match = /^\s*(\d+)\s+(.*)$/.exec(line);
1162
+ if (match && command.test(match[2])) pids.push(Number(match[1]));
1163
+ }
1164
+ return pids;
1165
+ }
1166
+ function listAvdEmulatorProcesses(avdName, platform) {
1167
+ if (platform === "win32") return [];
1168
+ const out = getExecutor().runFileQuiet("ps", [
1169
+ "-axww",
1170
+ "-o",
1171
+ "pid=,command="
1172
+ ], { timeoutMs: 5e3 });
1173
+ return out === null ? null : parseAvdEmulatorProcesses(out, avdName);
1174
+ }
1175
+ function scanAvdEmulatorProcesses(avdName, platform, listProcesses) {
1176
+ const pids = listProcesses(avdName, platform);
1177
+ if (pids === null) throw new Error(`Could not read the process table to verify that owned AVD ${avdName} stopped.`);
1178
+ return pids;
1179
+ }
1180
+ function assertOwnedAvdStopped(avdName, { processAlive = pidExists, listProcesses = listAvdEmulatorProcesses, ...resolveOptions } = {}) {
951
1181
  const { processId } = resolveAvdProcess(avdName, resolveOptions);
952
- if (processId !== null && processAlive(processId)) throw new Error(`Owned AVD ${avdName} still has a live emulator process (${processId}).`);
1182
+ const live = new Set(scanAvdEmulatorProcesses(avdName, resolveOptions.platform ?? process.platform, listProcesses));
1183
+ if (processId !== null && processAlive(processId)) live.add(processId);
1184
+ if (live.size) throw new Error(`Owned AVD ${avdName} still has a live emulator process (${[...live].join(", ")}).`);
953
1185
  }
954
1186
  const CRASH_HANDLER_EXIT_TIMEOUT_MS = 15e3;
955
1187
  /**
@@ -966,23 +1198,85 @@ function emulatorCrashHandlerPids(qemuPid) {
966
1198
  ], { timeoutMs: 1e4 });
967
1199
  return String(out ?? "").split(/\r?\n/).map((line) => Number(line.trim())).filter((pid) => Number.isSafeInteger(pid) && pid > 0);
968
1200
  }
969
- function waitForAndroidEmulatorShutdown(avdName, shutdown, { timeoutMs = ANDROID_EMULATOR_SHUTDOWN_TIMEOUT_MS, pollMs = ANDROID_EMULATOR_SHUTDOWN_POLL_MS, platform = process.platform, resolveDirectory = ownedAvdDirectory, readProcessId = readAvdProcessId, processAlive = pidExists, directoryExists = (path) => statSync$1(path).isDirectory(), crashHandlerPids = emulatorCrashHandlerPids, killCrashHandler = (pid) => signalProcessTree(pid, "SIGKILL", { platform }), now = Date.now, sleep = sleepSync } = {}) {
1201
+ const EMULATOR_SIGNAL_GRACE_MS = 5e3;
1202
+ /**
1203
+ * Shuts down the emulator of an owned AVD and returns only once every emulator process launched for it
1204
+ * has exited. `shutdown` asks the emulator to quit through its console; it is null when the emulator is
1205
+ * not reachable over adb. Processes still running after it get SIGTERM, and SIGKILL if they outlive their
1206
+ * grace, but only a process whose command line names this AVD and whose identity, captured before
1207
+ * shutdown, still matches. Windows has no process-table scan, so there nothing is signalled.
1208
+ */
1209
+ function waitForAndroidEmulatorShutdown(avdName, shutdown, { timeoutMs = ANDROID_EMULATOR_SHUTDOWN_TIMEOUT_MS, pollMs = ANDROID_EMULATOR_SHUTDOWN_POLL_MS, signalGraceMs = EMULATOR_SIGNAL_GRACE_MS, platform = process.platform, resolveDirectory = ownedAvdDirectory, readProcessId = readAvdProcessId, processAlive = pidExists, listProcesses = listAvdEmulatorProcesses, captureIdentity = process_identity_exports.captureProcessIdentity, inspectIdentity = process_identity_exports.inspectProcessIdentity, signal = (pid, name) => signalProcessTree(pid, name, { platform }), directoryExists = (path) => statSync(path).isDirectory(), crashHandlerPids = emulatorCrashHandlerPids, killCrashHandler = (pid) => signalProcessTree(pid, "SIGKILL", { platform }), now = Date.now, sleep = sleepSync } = {}) {
970
1210
  const { directory, processId } = resolveAvdProcess(avdName, {
971
1211
  platform,
972
1212
  resolveDirectory,
973
1213
  readProcessId
974
1214
  });
975
- if (processId === null) throw new Error(`Could not find the emulator process lock for owned AVD ${avdName}.`);
976
- const deadline = now() + timeoutMs;
977
- const shutdownTimeoutMs = deadline - now();
978
- if (shutdownTimeoutMs <= 0) throw new Error(`Owned AVD ${avdName} did not finish shutting down within ${Math.ceil(timeoutMs / 1e3)}s.`);
979
- shutdown(shutdownTimeoutMs);
980
- while (processAlive(processId)) {
981
- const remaining = deadline - now();
982
- if (remaining <= 0) throw new Error(`Owned AVD ${avdName} did not finish shutting down within ${Math.ceil(timeoutMs / 1e3)}s.`);
983
- sleep(Math.min(pollMs, remaining));
1215
+ const identities = /* @__PURE__ */ new Map();
1216
+ for (const pid of scanAvdEmulatorProcesses(avdName, platform, listProcesses)) {
1217
+ const captured = captureIdentity(pid);
1218
+ identities.set(pid, captured.ok ? {
1219
+ pid,
1220
+ processToken: captured.token
1221
+ } : null);
1222
+ }
1223
+ if (processId === null && identities.size === 0) {
1224
+ if (shutdown) throw new Error(`Could not find the emulator process lock for owned AVD ${avdName}.`);
1225
+ return;
1226
+ }
1227
+ const exited = (pid) => {
1228
+ const record = identities.get(pid);
1229
+ if (!record) return !processAlive(pid);
1230
+ const status = inspectIdentity(record);
1231
+ return status === "gone" || status === "different";
1232
+ };
1233
+ const running = () => {
1234
+ const pids = [...identities.keys()].filter((pid) => !exited(pid));
1235
+ if (processId !== null && !identities.has(processId) && processAlive(processId)) pids.push(processId);
1236
+ return pids;
1237
+ };
1238
+ const waitUntil = (until) => {
1239
+ for (;;) {
1240
+ if (running().length === 0) return true;
1241
+ const remaining = until - now();
1242
+ if (remaining <= 0) return false;
1243
+ sleep(Math.min(pollMs, remaining));
1244
+ }
1245
+ };
1246
+ const started = now();
1247
+ if (shutdown) {
1248
+ const deadline = started + timeoutMs;
1249
+ const shutdownTimeoutMs = deadline - now();
1250
+ if (shutdownTimeoutMs > 0) shutdown(shutdownTimeoutMs);
1251
+ waitUntil(deadline);
1252
+ }
1253
+ for (const name of ["SIGTERM", "SIGKILL"]) {
1254
+ const verified = running().filter((pid) => {
1255
+ const record = identities.get(pid);
1256
+ return record && inspectIdentity(record) === "same";
1257
+ });
1258
+ if (verified.length === 0) break;
1259
+ for (const pid of verified) try {
1260
+ signal(pid, name);
1261
+ } catch {}
1262
+ const graceMs = name === "SIGTERM" && !shutdown ? timeoutMs : signalGraceMs;
1263
+ if (waitUntil(now() + graceMs)) break;
1264
+ }
1265
+ const left = new Set(running());
1266
+ for (const pid of scanAvdEmulatorProcesses(avdName, platform, listProcesses)) {
1267
+ const record = identities.get(pid);
1268
+ if (record === void 0 || (record === null ? processAlive(pid) : inspectIdentity(record) !== "gone")) left.add(pid);
1269
+ }
1270
+ if (left.size) {
1271
+ const scanned = platform !== "win32";
1272
+ const unverified = [...left].filter((pid) => {
1273
+ const record = identities.get(pid);
1274
+ return scanned && (record === null || record !== void 0 && inspectIdentity(record) === "different");
1275
+ });
1276
+ const unlisted = [...left].filter((pid) => scanned && pid === processId && !identities.has(pid));
1277
+ throw new Error(`Owned AVD ${avdName} did not finish shutting down after ${Math.ceil((now() - started) / 1e3)}s: emulator process ${[...left].join(", ")} is still running` + (unverified.length ? ` (Stim could not verify the identity of ${unverified.join(", ")}, so it sent no signal)` : "") + (unlisted.length ? ` (${unlisted.join(", ")} holds the AVD process lock, but its command line does not name the AVD, so Stim sent no signal)` : "") + ".");
984
1278
  }
985
- if (platform === "win32") {
1279
+ if (platform === "win32" && processId !== null) {
986
1280
  const handlerDeadline = now() + CRASH_HANDLER_EXIT_TIMEOUT_MS;
987
1281
  for (const handler of crashHandlerPids(processId)) {
988
1282
  while (processAlive(handler) && now() < handlerDeadline) sleep(pollMs);
@@ -1029,12 +1323,14 @@ function ownedAvdSerialResolver({ timeoutMs } = {}) {
1029
1323
  const adb = once(() => listAdbDevices(remaining()));
1030
1324
  const names = /* @__PURE__ */ new Map();
1031
1325
  const avdNameOf = (serial) => {
1032
- if (!names.has(serial)) names.set(serial, getAvdNameForSerial(serial, remaining()));
1033
- return names.get(serial);
1326
+ if (names.has(serial)) return names.get(serial);
1327
+ const name = getAvdNameForSerial(serial, remaining());
1328
+ if (name !== null) names.set(serial, name);
1329
+ return name;
1034
1330
  };
1035
1331
  return (avdName) => {
1036
1332
  if (!avds().includes(avdName)) return { missing: true };
1037
- if (!isStimOwnedAvdName(avdName)) return { notOwned: true };
1333
+ if (!isStimOwnedAvd(avdName)) return { notOwned: true };
1038
1334
  const devices = adb();
1039
1335
  const match = [...devices.emulators, ...devices.unhealthy.filter((entry) => entry.kind === "emulator" && entry.consolePort !== void 0)].find((e) => avdNameOf(e.serial) === avdName);
1040
1336
  if (match) return { serial: match.serial };
@@ -1042,4 +1338,4 @@ function ownedAvdSerialResolver({ timeoutMs } = {}) {
1042
1338
  };
1043
1339
  }
1044
1340
  //#endregion
1045
- export { memoizeEmulatorProbe as A, probeEmulatorSerial as B, hostSystemImageArch as C, listAvds as D, listAvdDeviceProfiles as E, ownedAvdName as F, sleepSync as G, resolveOwnedAvdSerial as H, ownedAvdSerialResolver as I, wipeAvdUserData as J, waitForAndroidEmulatorShutdown as K, ownedAvdSystemImage as L, ownedAvdDeviceProfile as M, ownedAvdDirectory as N, listInstalledSystemImages as O, ownedAvdMatchesConfiguration as P, configuredAndroidEmulatorApp as Q, physicalDeviceModel as R, getAvdNameForSerial as S, listAdbDevices as T, resolvePhysicalDevice as U, resetAdoptedAvd as V, shutdownAndroidEmulator as W, forgetCreatedDevice as X, isStimOwnedSim as Y, recordCreatedDevice as Z, deleteAvd as _, androidPoolCandidates as a, extractEmulatorFailure as b, androidToolCwd as c, avdPathExists as d, avdPoolConfiguration as f, createOwnedAvd as g, configureNewOwnedAvd as h, androidHome as i, nextConsolePort as j, listOrphanedAvdDirectories as k, androidToolPath as l, bootAndroidEmulator as m, androidBuildToolName as n, androidPoolNoCandidatesRefusal as o, avdStorageRoots as p, waitForBoot as q, androidDeviceAbi as r, androidSystemImageAbi as s, DEFAULT_AVD_DEVICE_PROFILE as t, assertOwnedAvdStopped as u, emulatorDiskSpaceRemedy as v, isStimOwnedAvdName as w, findBuildTool as x, emulatorFailureRemedy as y, pickDefaultSystemImage as z };
1341
+ export { waitForBoot as $, listInstalledSystemImages as A, parseAvdDeviceProfile as B, extractEmulatorFailure as C, listAdbDevices as D, hostSystemImageArch as E, ownedAvdDirectory as F, profileNeedsFoldFeature as G, physicalDeviceModel as H, ownedAvdMatchesConfiguration as I, resolvePhysicalDevice as J, resetAdoptedAvd as K, ownedAvdName as L, memoizeEmulatorProbe as M, nextConsolePort as N, listAvdDeviceProfiles as O, ownedAvdDeviceProfile as P, waitForAndroidEmulatorShutdown as Q, ownedAvdSerialResolver as R, emulatorFailureRemedy as S, getAvdNameForSerial as T, pickDefaultSystemImage as U, parseAvdSystemImage as V, probeEmulatorSerial as W, sleepSync as X, shutdownAndroidEmulator as Y, systemImageSupportsFold as Z, bootAndroidEmulator as _, androidHome as a, dropParked as at, deleteAvd as b, androidSystemImageAbi as c, parkedMaxSetting as ct, assertOwnedAvdStopped as d, selectParked as dt, wipeAvdUserData as et, avdIniPaths as f, configuredAndroidEmulatorApp as ft, avdStorageRoots as g, avdPoolConfiguration as h, androidDeviceAbi as i, adoptParked as it, listOrphanedAvdDirectories as j, listAvds as k, androidToolCwd as l, readParked as lt, avdPathExists as m, OWNED_AVD_CONFIG_DEFAULTS as n, isStimOwnedSim as nt, androidPoolCandidates as o, isLegacyDeletionClaim as ot, avdNameAbsent as p, resolveOwnedAvdSerial as q, androidBuildToolName as r, POOL_SETTING_REMEDY as rt, androidPoolNoCandidatesRefusal as s, parkSim as st, DEFAULT_AVD_DEVICE_PROFILE as t, isStimOwnedAvd as tt, androidToolPath as u, removeParkedAfter as ut, configureNewOwnedAvd as v, findBuildTool as w, emulatorDiskSpaceRemedy as x, createOwnedAvd as y, ownedAvdSystemImage as z };