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.
- package/README.md +3 -1
- package/dist/{activity-BtG4Va9l.mjs → activity-DfVKZTEB.mjs} +44 -26
- package/dist/{android-BA2i_KAV.mjs → android-BHfAu2mf.mjs} +1 -1
- package/dist/{android-C1q4l523.mjs → android-CD8pr8Q2.mjs} +112 -54
- package/dist/{android-cas-DfJkwCXW.mjs → android-cas-B9Kv9aIy.mjs} +12 -10
- package/dist/{android-C4zG2Tuc.mjs → android-qx_31wJo.mjs} +414 -118
- package/dist/{app-install-DHut6JnV.mjs → app-install-CDFY2nUL.mjs} +1 -1
- package/dist/budget-E5o91NR1.mjs +1739 -0
- package/dist/{build-plan-CeAYgKAx.mjs → build-plan-BTa93o_r.mjs} +125 -107
- package/dist/{build-progress-CDwogz6R.mjs → build-progress-l7d-X-ZP.mjs} +76 -8
- package/dist/{cache-manifest-Pubfp8wV.mjs → cache-manifest-DWPF4RKJ.mjs} +1 -1
- package/dist/cache-manifest.mjs +1 -1
- package/dist/cdp-Dq_ypAlJ.mjs +115 -0
- package/dist/chrome-B5hWgQJS.mjs +61 -0
- package/dist/cli.mjs +17 -16
- package/dist/collector-run.mjs +15 -9
- package/dist/{command-output-D5z2_bcF.mjs → command-output-BIL7rlX7.mjs} +1 -1
- package/dist/{config-BEq0Qw_a.mjs → config-0jCcMM0E.mjs} +10 -1
- package/dist/created-devices-Cd558h31.mjs +37 -0
- package/dist/{dependency-state-r9UesA9a.mjs → dependency-state-DNN0yfx6.mjs} +1 -1
- package/dist/{deps-OF7CD5Dx.mjs → deps-2Wo81np9.mjs} +2 -2
- package/dist/{device-BwH2iI3O.mjs → device-Cf7vl-so.mjs} +7 -7
- package/dist/{device-lease-D83nqeMX.mjs → device-lease-CL9dZf73.mjs} +55 -6
- package/dist/{device-pool-DHj7cdPy.mjs → device-pool-MuoiyRcu.mjs} +4 -4
- package/dist/{device-remote-BQZSnnmE.mjs → device-remote-DT9ngZiD.mjs} +254 -579
- package/dist/{doctor-D8QdMRMf.mjs → doctor-D2iysNNH.mjs} +50 -20
- package/dist/{error-diagnostics-UYFSQwRy.mjs → error-diagnostics-BoiOhck8.mjs} +7 -7
- package/dist/{gc-COZteyiq.mjs → gc-eA-PRBX8.mjs} +1022 -131
- package/dist/{guide-NDvTmMi-.mjs → guide-BfZ0OmiP.mjs} +909 -132
- package/dist/{guide-status-CWvti2Lg.mjs → guide-status-BaMYu55M.mjs} +2 -2
- package/dist/{idle-NETbeNiQ.mjs → idle-DxLi5OkN.mjs} +43 -10
- package/dist/{in-use-DVhlSoyn.mjs → in-use-wYEaKQM9.mjs} +11 -7
- package/dist/{ios-CQLKpNfr.mjs → ios-CcfCeZ19.mjs} +103 -58
- package/dist/{ios-DArbmwer.mjs → ios-CmEzZiOj.mjs} +31 -22
- package/dist/{ios-device-CwDZo0X4.mjs → ios-device-BW-t_Lkv.mjs} +1 -1
- package/dist/{launch-verify-CuPDGXdr.mjs → launch-verify-CuJaBpEa.mjs} +33 -20
- package/dist/{logs-B44a1UeU.mjs → logs-DcSRbC2t.mjs} +12 -11
- package/dist/{merge-state-DDxS-XIZ.mjs → merge-state-ytFQCeh4.mjs} +38 -8
- package/dist/{metro-BWLOJ_Wr.mjs → metro-BPeY1YMh.mjs} +5 -5
- package/dist/{named-ports-Dc7neAlc.mjs → named-ports-n7tnJsI8.mjs} +78 -6
- package/dist/native-runtime-CiiFHt0e.mjs +74 -0
- package/dist/{ownership-D2xbLVoz.mjs → ownership-Bz2kETAS.mjs} +2 -2
- package/dist/{ownership-BiojdtAp.mjs → ownership-PUP4Dwai.mjs} +175 -217
- package/dist/page-BTu7I60K.mjs +29 -0
- package/dist/{ports-DwBaRFJs.mjs → ports-C5UutqpJ.mjs} +5 -5
- package/dist/{project-BXZzOABt.mjs → project-DGeclhvO.mjs} +55 -87
- package/dist/{reload-By9Dwsrt.mjs → reload-C-cV5yy_.mjs} +80 -21
- package/dist/remote-cache--aehr4Pr.mjs +526 -0
- package/dist/{server-bare-D_RK1RCj.mjs → server-bare-Bq8BpSIo.mjs} +1 -1
- package/dist/server-expo-BeYwP_Xy.mjs +2 -0
- package/dist/{server-expo-o6H5gcBc.mjs → server-expo-DUCg52-K.mjs} +2 -2
- package/dist/{settings-C0ugALSC.mjs → settings-ssVBToDR.mjs} +29 -17
- package/dist/{settings-B5oFZEri.mjs → settings-xB0Frkg6.mjs} +13 -4
- package/dist/settings.schema.json +74 -2
- package/dist/{simslim-CIR3IYJO.mjs → simslim-Bbg9UWrt.mjs} +13 -6
- package/dist/{slot-launch-rggO1Esw.mjs → slot-launch-BIWEW-wI.mjs} +4 -4
- package/dist/{spawn-claims-BJwXTMPt.mjs → spawn-claims-PT8aQpCa.mjs} +1 -1
- package/dist/{start-BSsf2tUs.mjs → start-BZhItkiL.mjs} +110 -18
- package/dist/{start-i8zc9VmS.mjs → start-Rw2GD3nV.mjs} +1 -1
- package/dist/{state-WGdIbi_d.mjs → state-DwAU2_-D.mjs} +15 -3
- package/dist/state-l1pYwOre.mjs +148 -0
- package/dist/{stats-DBYJ5bxL.mjs → stats-CaSwmsSZ.mjs} +4 -4
- package/dist/{stats-BAgJaLA4.mjs → stats-D8rUTPHd.mjs} +1 -1
- package/dist/{status-CitcBRsC.mjs → status-TJlL0HWo.mjs} +649 -116
- package/dist/{status-Q-WDEGCB.mjs → status-sszP4XKU.mjs} +130 -28
- package/dist/stim-desktop-CXt8-Z3J.mjs +40 -0
- package/dist/{stop-DIhrvu0B.mjs → stop-BZ2qMiBP.mjs} +50 -18
- package/dist/{stop-rst5mL14.mjs → stop-DskDKNTU.mjs} +2 -2
- package/dist/supervisor-run.mjs +11 -11
- package/dist/web-run.d.mts +22 -0
- package/dist/web-run.mjs +481 -0
- package/dist/web-sWXd5iek.mjs +394 -0
- package/dist/{worktree-DhxDTK44.mjs → worktree-3cBa-bQt.mjs} +1 -1
- package/dist/{worktree-KOjMX3zA.mjs → worktree-D4Do_XiQ.mjs} +229 -21
- package/dist/{worktree-BlizKDpZ.mjs → worktree-DeBQSVtR.mjs} +3 -1
- package/dist/{xcode-B5SPD--o.mjs → xcode-LM6A8cNZ.mjs} +7 -7
- package/package.json +5 -5
- package/dist/budget-De3P2rq_.mjs +0 -620
- package/dist/native-run-D7yB_AsB.mjs +0 -143
- package/dist/server-expo-B-CCUJZj.mjs +0 -2
- package/dist/workspace-process-lock-Cx74RSDZ.mjs +0 -57
- package/dist/workspaces-COD2xCz-.mjs +0 -1013
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
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,
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
217
|
+
return removed;
|
|
218
|
+
} finally {
|
|
219
|
+
(0, ownership_claim_exports.releaseClaim)(claim);
|
|
220
|
+
}
|
|
57
221
|
}
|
|
58
|
-
function
|
|
59
|
-
|
|
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
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
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
|
|
80
|
-
|
|
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
|
|
84
|
-
return
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
175
|
-
|
|
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
|
|
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
|
|
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
|
|
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 (!
|
|
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
|
|
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
|
|
551
|
-
names: readdirSync
|
|
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
|
|
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
|
|
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
|
|
576
|
-
if (!stat.isDirectory() || stat.isSymbolicLink() || realpathSync
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
821
|
-
const fd = openSync
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
976
|
-
const
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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 (
|
|
1033
|
-
|
|
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 (!
|
|
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 {
|
|
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 };
|