stim 1.12.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 (71) hide show
  1. package/README.md +3 -1
  2. package/dist/{activity-CCZJs__P.mjs → activity-DfVKZTEB.mjs} +42 -25
  3. package/dist/{android-BQiKJYK0.mjs → android-BHfAu2mf.mjs} +1 -1
  4. package/dist/{android-B7X-8H7h.mjs → android-CD8pr8Q2.mjs} +62 -47
  5. package/dist/{android-cas-DzLPXiEn.mjs → android-cas-B9Kv9aIy.mjs} +9 -7
  6. package/dist/{android-75Sk2rIR.mjs → android-qx_31wJo.mjs} +395 -116
  7. package/dist/{app-install-CsZi0SAT.mjs → app-install-CDFY2nUL.mjs} +1 -1
  8. package/dist/{budget-DbzW5VTD.mjs → budget-E5o91NR1.mjs} +154 -37
  9. package/dist/{build-plan-CqntbR7b.mjs → build-plan-BTa93o_r.mjs} +21 -88
  10. package/dist/{build-progress-h60GPV27.mjs → build-progress-l7d-X-ZP.mjs} +75 -7
  11. package/dist/cdp-Dq_ypAlJ.mjs +115 -0
  12. package/dist/chrome-B5hWgQJS.mjs +61 -0
  13. package/dist/cli.mjs +16 -15
  14. package/dist/collector-run.mjs +14 -8
  15. package/dist/{command-output-BWCj5Vh-.mjs → command-output-BIL7rlX7.mjs} +1 -1
  16. package/dist/created-devices-Cd558h31.mjs +37 -0
  17. package/dist/{deps-CO-_7UOm.mjs → deps-2Wo81np9.mjs} +1 -1
  18. package/dist/{device-9r9xRYDa.mjs → device-Cf7vl-so.mjs} +6 -6
  19. package/dist/{device-lease-B9LAXIUH.mjs → device-lease-CL9dZf73.mjs} +52 -3
  20. package/dist/{device-pool-BoezJd-c.mjs → device-pool-MuoiyRcu.mjs} +3 -3
  21. package/dist/{device-remote-C0_o-tJH.mjs → device-remote-DT9ngZiD.mjs} +10 -10
  22. package/dist/{doctor-LfDlbR-N.mjs → doctor-D2iysNNH.mjs} +47 -18
  23. package/dist/{error-diagnostics-CCxjn31V.mjs → error-diagnostics-BoiOhck8.mjs} +6 -6
  24. package/dist/{gc-C4nQFcTX.mjs → gc-eA-PRBX8.mjs} +853 -126
  25. package/dist/{guide-BPBlh9ML.mjs → guide-BfZ0OmiP.mjs} +736 -110
  26. package/dist/{guide-status-i9ogHt4v.mjs → guide-status-BaMYu55M.mjs} +1 -1
  27. package/dist/{idle-S3tFZ53_.mjs → idle-DxLi5OkN.mjs} +42 -9
  28. package/dist/{in-use-D9XYEgFD.mjs → in-use-wYEaKQM9.mjs} +9 -5
  29. package/dist/{ios-CDzWL1mj.mjs → ios-CcfCeZ19.mjs} +98 -54
  30. package/dist/{ios-CZWOHTTc.mjs → ios-CmEzZiOj.mjs} +10 -5
  31. package/dist/{ios-device-D9cLxVr5.mjs → ios-device-BW-t_Lkv.mjs} +1 -1
  32. package/dist/{launch-verify-B2oR_kFN.mjs → launch-verify-CuJaBpEa.mjs} +33 -20
  33. package/dist/{logs-SzuYudOj.mjs → logs-DcSRbC2t.mjs} +11 -10
  34. package/dist/{metro-Dzo30hvJ.mjs → metro-BPeY1YMh.mjs} +4 -4
  35. package/dist/{named-ports-Dn1o61D9.mjs → named-ports-n7tnJsI8.mjs} +78 -6
  36. package/dist/native-runtime-CiiFHt0e.mjs +74 -0
  37. package/dist/{ownership-Dqnrjrio.mjs → ownership-Bz2kETAS.mjs} +1 -1
  38. package/dist/{ownership-B8d4kXBx.mjs → ownership-PUP4Dwai.mjs} +134 -209
  39. package/dist/page-BTu7I60K.mjs +29 -0
  40. package/dist/{ports-Dp_QTwTC.mjs → ports-C5UutqpJ.mjs} +4 -4
  41. package/dist/{project-B8rbutVC.mjs → project-DGeclhvO.mjs} +53 -17
  42. package/dist/{reload-nocmx8KB.mjs → reload-C-cV5yy_.mjs} +79 -20
  43. package/dist/{remote-cache-BRsRZru5.mjs → remote-cache--aehr4Pr.mjs} +3 -3
  44. package/dist/{server-bare-Gn3ssSdK.mjs → server-bare-Bq8BpSIo.mjs} +1 -1
  45. package/dist/server-expo-BeYwP_Xy.mjs +2 -0
  46. package/dist/{server-expo-CeT1nIIk.mjs → server-expo-DUCg52-K.mjs} +2 -2
  47. package/dist/{settings-bE-Gu3Ti.mjs → settings-ssVBToDR.mjs} +10 -10
  48. package/dist/{settings-BlyDn2Oz.mjs → settings-xB0Frkg6.mjs} +11 -2
  49. package/dist/settings.schema.json +53 -1
  50. package/dist/{simslim-COtEEP7x.mjs → simslim-Bbg9UWrt.mjs} +12 -6
  51. package/dist/{slot-launch-DIACGEfH.mjs → slot-launch-BIWEW-wI.mjs} +3 -3
  52. package/dist/{start-BboE1HIS.mjs → start-BZhItkiL.mjs} +11 -10
  53. package/dist/{start-BFstVBlx.mjs → start-Rw2GD3nV.mjs} +1 -1
  54. package/dist/{state-DcDgzfu3.mjs → state-DwAU2_-D.mjs} +1 -1
  55. package/dist/state-l1pYwOre.mjs +148 -0
  56. package/dist/{stats-DiCtmluY.mjs → stats-CaSwmsSZ.mjs} +2 -2
  57. package/dist/{status-DI9Tjo7T.mjs → status-TJlL0HWo.mjs} +624 -103
  58. package/dist/{status-BUASYyy_.mjs → status-sszP4XKU.mjs} +59 -2
  59. package/dist/{stim-desktop-D0xhpZt8.mjs → stim-desktop-CXt8-Z3J.mjs} +13 -2
  60. package/dist/{stop-Bn6v-GvS.mjs → stop-BZ2qMiBP.mjs} +37 -12
  61. package/dist/{stop-BI7QY76M.mjs → stop-DskDKNTU.mjs} +2 -2
  62. package/dist/supervisor-run.mjs +10 -10
  63. package/dist/web-run.d.mts +22 -0
  64. package/dist/web-run.mjs +481 -0
  65. package/dist/web-sWXd5iek.mjs +394 -0
  66. package/dist/{worktree-B5sTRskL.mjs → worktree-3cBa-bQt.mjs} +1 -1
  67. package/dist/{worktree-ChDL5fO1.mjs → worktree-D4Do_XiQ.mjs} +226 -19
  68. package/dist/{xcode-BzplpQzc.mjs → xcode-LM6A8cNZ.mjs} +5 -5
  69. package/package.json +5 -5
  70. package/dist/server-expo-eWi9Coof.mjs +0 -2
  71. package/dist/workspace-process-lock-BvvJTfo_.mjs +0 -57
@@ -1,13 +1,15 @@
1
- import { _ as loadConfig$1, d as getConfigDir } from "./config-0jCcMM0E.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-Dzo30hvJ.mjs";
4
- import { n as stimDesktopInstalled, t as settingDefault } from "./stim-desktop-D0xhpZt8.mjs";
5
- import { c as androidDataPartitionSizeBytes } from "./settings-BlyDn2Oz.mjs";
6
- import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
7
- 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";
8
8
  import { dirname, isAbsolute, join, resolve } from "path";
9
- 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";
10
11
  import { homedir, tmpdir } from "os";
12
+ import { isDeepStrictEqual } from "node:util";
11
13
  //#region src/devices/android-emulator-viewer.ts
12
14
  function configuredAndroidEmulatorApp(platform = process.platform) {
13
15
  const value = loadConfig$1()?.androidEmulatorApp ?? settingDefault(settingDefinition("androidEmulatorApp"), () => stimDesktopInstalled(platform)).value;
@@ -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);
247
+ }
248
+ function isStimOwnedSim(sim) {
249
+ return (sim.name ?? "").startsWith("stim-") && isStimOwnedDevice("ios", sim.udid);
82
250
  }
83
- function isStimOwnedAvd(name, readConfig, created = readCreatedDevices$1()) {
84
- return isOwnedAvdName(name) && (created.android.has(name) || avdConfigWrittenByStim(readConfig()));
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
  }
@@ -211,7 +387,7 @@ function profileNeedsFoldFeature(profile) {
211
387
  }
212
388
  function systemImageSupportsFold(pkg, home = androidHome()) {
213
389
  try {
214
- const ini = readFileSync$1(join(home, ...pkg.split(";"), "advancedFeatures.ini"), "utf8");
390
+ const ini = readFileSync(join(home, ...pkg.split(";"), "advancedFeatures.ini"), "utf8");
215
391
  return /^\s*SupportPixelFold\s*=\s*on\s*$/im.test(ini);
216
392
  } catch {
217
393
  return false;
@@ -283,7 +459,7 @@ function avdIniValue(configIni, key) {
283
459
  }
284
460
  return null;
285
461
  }
286
- function ownedAvdSystemImage(avdName, { avdDirectory = ownedAvdDirectory, readFile = (path) => readFileSync$1(path, "utf8") } = {}) {
462
+ function ownedAvdSystemImage(avdName, { avdDirectory = ownedAvdDirectory, readFile = (path) => readFileSync(path, "utf8") } = {}) {
287
463
  const directory = avdDirectory(avdName);
288
464
  if (!directory) return null;
289
465
  try {
@@ -310,24 +486,18 @@ function sanitizeAvdLabel(label) {
310
486
  function avdConfigIni(avdName, directory = ownedAvdDirectory(avdName)) {
311
487
  if (!directory) return null;
312
488
  try {
313
- return readFileSync$1(join(directory, "config.ini"), "utf8");
489
+ return readFileSync(join(directory, "config.ini"), "utf8");
314
490
  } catch {
315
491
  return null;
316
492
  }
317
493
  }
318
- function registeredOrOrphanedAvdDirectory(avdName) {
319
- return ownedAvdDirectory(avdName) ?? listOrphanedAvdDirectories(avdName)[0]?.directory ?? null;
320
- }
321
- function isStimOwnedAvdName(avdName, directory) {
322
- return isStimOwnedAvd(avdName, () => avdConfigIni(avdName, directory === void 0 ? registeredOrOrphanedAvdDirectory(avdName) : directory));
323
- }
324
494
  function ownedAvdName(label) {
325
495
  const clean = sanitizeAvdLabel(label);
326
496
  return `stim-${clean.startsWith("stim-") ? clean.slice(5) : clean}`;
327
497
  }
328
498
  const AVDMANAGER_DELETE_TIMEOUT_MS = 12e4;
329
499
  function deleteAvd(avdName) {
330
- 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.`);
331
501
  getExecutor().run(`${androidTool("avdmanager")} delete avd -n "${avdName}"`, {
332
502
  timeoutMs: AVDMANAGER_DELETE_TIMEOUT_MS,
333
503
  killSignal: "SIGKILL"
@@ -548,28 +718,45 @@ function avdStorageRoots() {
548
718
  }
549
719
  function avdPathExists(path) {
550
720
  try {
551
- lstatSync$1(path);
721
+ lstatSync(path);
552
722
  return true;
553
723
  } catch (error) {
554
724
  if (error.code === "ENOENT") return false;
555
725
  throw error;
556
726
  }
557
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
+ }
558
745
  function listOrphanedAvdDirectories(avdName) {
559
746
  const roots = avdStorageRoots();
560
747
  const stores = roots.filter(avdPathExists).map((root) => ({
561
748
  root,
562
- canonicalRoot: realpathSync$1(root),
563
- names: readdirSync$1(root)
749
+ canonicalRoot: realpathSync(root),
750
+ names: readdirSync(root)
564
751
  }));
565
752
  const registered = /* @__PURE__ */ new Set();
566
753
  for (const { root, canonicalRoot, names } of stores) for (const name of names.filter((entry) => entry.endsWith(".ini"))) {
567
754
  const path = join(root, name);
568
- const ini = readFileSync$1(path, "utf8");
755
+ const ini = readFileSync(path, "utf8");
569
756
  const targets = /* @__PURE__ */ new Set([...avdIniPaths(root, ini), ...avdIniPaths(canonicalRoot, ini)]);
570
757
  if (!targets.size) throw new Error(`Cannot verify AVD registration at ${path}; its data was kept.`);
571
758
  for (const target of targets) try {
572
- registered.add(realpathSync$1(target));
759
+ registered.add(realpathSync(target));
573
760
  } catch (error) {
574
761
  const code = error.code;
575
762
  if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
@@ -584,8 +771,8 @@ function listOrphanedAvdDirectories(avdName) {
584
771
  if (roots.some((candidate) => avdPathExists(join(candidate, `${name}.ini`)))) continue;
585
772
  const directory = join(canonicalRoot, entry);
586
773
  if (registered.has(directory) || !avdPathExists(directory)) continue;
587
- const stat = lstatSync$1(directory);
588
- 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.`);
589
776
  if (!orphaned.some((candidate) => candidate.directory === directory)) orphaned.push({
590
777
  name,
591
778
  directory,
@@ -596,7 +783,7 @@ function listOrphanedAvdDirectories(avdName) {
596
783
  }
597
784
  return orphaned;
598
785
  }
599
- 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() } = {}) {
600
787
  if (!/^stim-[A-Za-z0-9._-]+$/.test(avdName)) return null;
601
788
  const roots = [
602
789
  env.ANDROID_AVD_HOME,
@@ -630,7 +817,7 @@ const AVD_USER_DATA = [
630
817
  "snapshots"
631
818
  ];
632
819
  function wipeAvdUserData(directory) {
633
- for (const name of AVD_USER_DATA) rmSync$1(join(directory, name), {
820
+ for (const name of AVD_USER_DATA) rmSync(join(directory, name), {
634
821
  recursive: true,
635
822
  force: true
636
823
  });
@@ -656,10 +843,10 @@ function withAvdConfigOverrides(contents, overrides) {
656
843
  return updated.join(newline) + (trailingNewline ? newline : "");
657
844
  }
658
845
  let avdConfigWriteSequence = 0;
659
- 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, {
660
847
  encoding: "utf8",
661
848
  flag: "wx"
662
- }), rename = renameSync$1, remove = (path) => rmSync$1(path, { force: true }) } = {}) {
849
+ }), rename = renameSync, remove = (path) => rmSync(path, { force: true }) } = {}) {
663
850
  const sizeBytes = androidDataPartitionSizeBytes(dataPartitionSizeGb);
664
851
  const directory = avdDirectory(avdName);
665
852
  if (!directory) throw new Error(`Could not resolve the content directory for newly created AVD ${avdName}.`);
@@ -699,7 +886,7 @@ function avdPoolConfiguration(dataPartitionSizeGb, avdConfig, deviceProfile = DE
699
886
  function ownedAvdMatchesConfiguration(avdName, configuration) {
700
887
  const directory = ownedAvdDirectory(avdName);
701
888
  if (!directory) return false;
702
- const contents = readFileSync$1(join(directory, "config.ini"), "utf8");
889
+ const contents = readFileSync(join(directory, "config.ini"), "utf8");
703
890
  const expected = JSON.parse(configuration);
704
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]}`));
705
892
  }
@@ -782,7 +969,7 @@ const CRASH_REPORT_MODE_EMULATOR_VERSION = 37;
782
969
  function emulatorCrashDatabases(env = process.env) {
783
970
  const root = join(env.TMP || env.TEMP || tmpdir(), "AndroidEmulator");
784
971
  try {
785
- 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));
786
973
  } catch {
787
974
  return [];
788
975
  }
@@ -796,7 +983,7 @@ function emulatorCrashDatabases(env = process.env) {
796
983
  * removed instead, which has the same effect unless a crash handler is still writing into it.
797
984
  * https://github.com/appandflow/stim/issues/918
798
985
  */
799
- 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, {
800
987
  recursive: true,
801
988
  force: true
802
989
  }) } = {}) {
@@ -834,8 +1021,8 @@ function bootAndroidEmulator(avdName, consolePort, { logFile, platform = process
834
1021
  function emulatorStdio(logFile) {
835
1022
  if (!logFile) return "ignore";
836
1023
  try {
837
- mkdirSync$1(dirname(logFile), { recursive: true });
838
- const fd = openSync$1(logFile, "w");
1024
+ mkdirSync(dirname(logFile), { recursive: true });
1025
+ const fd = openSync(logFile, "w");
839
1026
  return [
840
1027
  "ignore",
841
1028
  fd,
@@ -940,7 +1127,7 @@ function avdProcessLockPaths(avdDirectory, platform) {
940
1127
  }
941
1128
  function readAvdProcessId(path) {
942
1129
  try {
943
- const pid = Number(readFileSync$1(path, "utf8").split("\0")[0]);
1130
+ const pid = Number(readFileSync(path, "utf8").split("\0")[0]);
944
1131
  if (!Number.isSafeInteger(pid) || pid <= 0) throw new Error(`Could not read the emulator PID from process lock ${path}.`);
945
1132
  return pid;
946
1133
  } catch (error) {
@@ -964,9 +1151,37 @@ function resolveAvdProcess(avdName, { platform = process.platform, resolveDirect
964
1151
  processId: null
965
1152
  };
966
1153
  }
967
- 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 } = {}) {
968
1181
  const { processId } = resolveAvdProcess(avdName, resolveOptions);
969
- 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(", ")}).`);
970
1185
  }
971
1186
  const CRASH_HANDLER_EXIT_TIMEOUT_MS = 15e3;
972
1187
  /**
@@ -983,23 +1198,85 @@ function emulatorCrashHandlerPids(qemuPid) {
983
1198
  ], { timeoutMs: 1e4 });
984
1199
  return String(out ?? "").split(/\r?\n/).map((line) => Number(line.trim())).filter((pid) => Number.isSafeInteger(pid) && pid > 0);
985
1200
  }
986
- 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 } = {}) {
987
1210
  const { directory, processId } = resolveAvdProcess(avdName, {
988
1211
  platform,
989
1212
  resolveDirectory,
990
1213
  readProcessId
991
1214
  });
992
- if (processId === null) throw new Error(`Could not find the emulator process lock for owned AVD ${avdName}.`);
993
- const deadline = now() + timeoutMs;
994
- const shutdownTimeoutMs = deadline - now();
995
- if (shutdownTimeoutMs <= 0) throw new Error(`Owned AVD ${avdName} did not finish shutting down within ${Math.ceil(timeoutMs / 1e3)}s.`);
996
- shutdown(shutdownTimeoutMs);
997
- while (processAlive(processId)) {
998
- const remaining = deadline - now();
999
- if (remaining <= 0) throw new Error(`Owned AVD ${avdName} did not finish shutting down within ${Math.ceil(timeoutMs / 1e3)}s.`);
1000
- 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)` : "") + ".");
1001
1278
  }
1002
- if (platform === "win32") {
1279
+ if (platform === "win32" && processId !== null) {
1003
1280
  const handlerDeadline = now() + CRASH_HANDLER_EXIT_TIMEOUT_MS;
1004
1281
  for (const handler of crashHandlerPids(processId)) {
1005
1282
  while (processAlive(handler) && now() < handlerDeadline) sleep(pollMs);
@@ -1046,12 +1323,14 @@ function ownedAvdSerialResolver({ timeoutMs } = {}) {
1046
1323
  const adb = once(() => listAdbDevices(remaining()));
1047
1324
  const names = /* @__PURE__ */ new Map();
1048
1325
  const avdNameOf = (serial) => {
1049
- if (!names.has(serial)) names.set(serial, getAvdNameForSerial(serial, remaining()));
1050
- 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;
1051
1330
  };
1052
1331
  return (avdName) => {
1053
1332
  if (!avds().includes(avdName)) return { missing: true };
1054
- if (!isStimOwnedAvdName(avdName)) return { notOwned: true };
1333
+ if (!isStimOwnedAvd(avdName)) return { notOwned: true };
1055
1334
  const devices = adb();
1056
1335
  const match = [...devices.emulators, ...devices.unhealthy.filter((entry) => entry.kind === "emulator" && entry.consolePort !== void 0)].find((e) => avdNameOf(e.serial) === avdName);
1057
1336
  if (match) return { serial: match.serial };
@@ -1059,4 +1338,4 @@ function ownedAvdSerialResolver({ timeoutMs } = {}) {
1059
1338
  };
1060
1339
  }
1061
1340
  //#endregion
1062
- export { forgetCreatedDevice as $, listOrphanedAvdDirectories as A, pickDefaultSystemImage as B, getAvdNameForSerial as C, listAvdDeviceProfiles as D, listAdbDevices as E, ownedAvdMatchesConfiguration as F, resolvePhysicalDevice as G, profileNeedsFoldFeature as H, ownedAvdName as I, systemImageSupportsFold as J, shutdownAndroidEmulator as K, ownedAvdSerialResolver as L, nextConsolePort as M, ownedAvdDeviceProfile as N, listAvds as O, ownedAvdDirectory as P, isStimOwnedSim as Q, ownedAvdSystemImage as R, findBuildTool as S, isStimOwnedAvdName as T, resetAdoptedAvd as U, probeEmulatorSerial as V, resolveOwnedAvdSerial as W, waitForBoot as X, waitForAndroidEmulatorShutdown as Y, wipeAvdUserData as Z, createOwnedAvd as _, androidHome as a, emulatorFailureRemedy as b, androidSystemImageAbi as c, assertOwnedAvdStopped as d, recordCreatedDevice as et, avdPathExists as f, configureNewOwnedAvd as g, bootAndroidEmulator as h, androidDeviceAbi as i, memoizeEmulatorProbe as j, listInstalledSystemImages as k, androidToolCwd as l, avdStorageRoots as m, OWNED_AVD_CONFIG_DEFAULTS as n, androidPoolCandidates as o, avdPoolConfiguration as p, sleepSync as q, androidBuildToolName as r, androidPoolNoCandidatesRefusal as s, DEFAULT_AVD_DEVICE_PROFILE as t, configuredAndroidEmulatorApp as tt, androidToolPath as u, deleteAvd as v, hostSystemImageArch as w, extractEmulatorFailure as x, emulatorDiskSpaceRemedy as y, physicalDeviceModel 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 };