stim 1.14.0 → 1.16.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 +2 -0
- package/dist/activity-BxwdD81S.mjs +609 -0
- package/dist/agent-device-usage-output-Dpih1Tfa.mjs +411 -0
- package/dist/{android-d6rGq1pA.mjs → android-B_NumVyQ.mjs} +169 -81
- package/dist/{android-LbPFRWVL.mjs → android-Ift4kVvt.mjs} +517 -413
- package/dist/android-cas-8U4DPqeR.mjs +167 -0
- package/dist/android-cas-compiler.mjs +2 -2
- package/dist/{android-XpvCrV7q.mjs → android-r2rBL8kV.mjs} +2 -2
- package/dist/{app-install-BpgSgkJj.mjs → app-install-CKWdbXIX.mjs} +128 -151
- package/dist/attempt-Bk2ouQVL.mjs +39 -0
- package/dist/budget-DjrURfQn.mjs +2700 -0
- package/dist/{build-plan-Dcr-3ie0.mjs → build-plan-DjeSb2Xl.mjs} +192 -653
- package/dist/build-slots-rUk77xqh.mjs +132 -0
- package/dist/{cache-manifest-C68QQ0ua.mjs → cache-manifest-DnUHBJaU.mjs} +2 -2
- package/dist/cache-manifest.mjs +1 -1
- package/dist/{chrome-BPCiC3ZX.mjs → chrome-BrcVWQyD.mjs} +1 -1
- package/dist/cli-BkYZ_6hy.mjs +51 -0
- package/dist/cli.mjs +6 -42
- package/dist/client-BTY686R6.mjs +1368 -0
- package/dist/collector-run.mjs +9 -195
- package/dist/{command-output-DVcpw6eE.mjs → command-output-DnJMdEY-.mjs} +1 -1
- package/dist/{config-lvc7d3C2.mjs → config-D0DbpZtM.mjs} +28 -29
- package/dist/{created-devices-CllH_8eb.mjs → created-devices-D5owjN4C.mjs} +2 -2
- package/dist/{dependency-state-DsT7aKeo.mjs → dependency-state-B6cMXZab.mjs} +2 -2
- package/dist/{deps-D9IcKqXS.mjs → deps-urbmfXig.mjs} +3 -3
- package/dist/{detached-entry-DJHoca49.mjs → detached-entry-CiTEgMR1.mjs} +1 -1
- package/dist/dev-client-CroAIjjj.mjs +43 -0
- package/dist/{device-CjuuAJjQ.mjs → device-DZQjF7H5.mjs} +8 -7
- package/dist/device-capacity-BHwxNBlu.mjs +136 -0
- package/dist/device-host-worker.d.mts +1 -0
- package/dist/device-host-worker.mjs +964 -0
- package/dist/device-ios-BadmeL2V.mjs +381 -0
- package/dist/{device-lease-5c3WrVRm.mjs → device-lease-D-hLqx1b.mjs} +43 -36
- package/dist/{device-pool-BiyDyPP-.mjs → device-lease-run-CqjShqWW.mjs} +6 -126
- package/dist/device-pool-CUaEWMMm.mjs +125 -0
- package/dist/{device-remote-D87zEzaq.mjs → device-remote-D5WVWqOq.mjs} +69 -163
- package/dist/{doctor-x9pnU9fq.mjs → doctor-Cimf-gGB.mjs} +241 -103
- package/dist/{error-diagnostics-C5HzcwCW.mjs → error-diagnostics-B9L4Ae0x.mjs} +15 -18
- package/dist/{exec-DK8KaVHK.mjs → exec-CaErVv7t.mjs} +5 -1
- package/dist/{gc-ko6mUAYD.mjs → gc-Bqy4bjEu.mjs} +599 -108
- package/dist/{android-cas-iZK3DOCr.mjs → gradle-BsO1MdsH.mjs} +8 -294
- package/dist/{guide-6R0VNFqI.mjs → guide-B_uyKTxW.mjs} +2511 -239
- package/dist/{guide-status-BZrFPIA_.mjs → guide-status-CjEZMAD4.mjs} +3 -3
- package/dist/hosted-client-a-WmqE2L.mjs +197 -0
- package/dist/hosted-ios-CIWelIqf.mjs +220 -0
- package/dist/hosted-logs-D5g041Q3.mjs +187 -0
- package/dist/hosted-macos-BSZyIb3y.mjs +135 -0
- package/dist/idle-DYVymG92.mjs +1099 -0
- package/dist/idle-shutdown-CTsDnAde.mjs +122 -0
- package/dist/in-use-DvBw0Kxo.mjs +130 -0
- package/dist/{ios-BwDPrnYQ.mjs → ios-BaInn3sW.mjs} +201 -10
- package/dist/ios-Cr1h5gnO.mjs +189 -0
- package/dist/{ios-P1f5nm2n.mjs → ios-aq3QoGI3.mjs} +1159 -633
- package/dist/{ios-device-BiOsSVzd.mjs → ios-device-Ee7sDXfp.mjs} +1 -1
- package/dist/{ios-device-DGglU3E_.mjs → ios-device-FfCN_C8p.mjs} +12 -7
- package/dist/ios-state-DS5UpYTH.mjs +48 -0
- package/dist/{launch-verify-Deg4tB3Z.mjs → launch-verify-2DBRizjo.mjs} +5 -5
- package/dist/logs-CHEVK8zD.mjs +371 -0
- package/dist/machines-BIBUdhpj.mjs +265 -0
- package/dist/macos-CSYEAFmF.mjs +473 -0
- package/dist/macos-run.d.mts +5 -0
- package/dist/macos-run.mjs +2 -0
- package/dist/maintenance-run.d.mts +4 -0
- package/dist/maintenance-run.mjs +254 -0
- package/dist/{metro-CcEo8Fl8.mjs → metro-BeK1ZPz1.mjs} +20 -120
- package/dist/metro-gateway-BJ-rxQAB.mjs +84 -0
- package/dist/{named-ports-Blg8IxOh.mjs → named-ports-Bk_YXEmE.mjs} +6 -59
- package/dist/native-run-NaBrRFtQ.mjs +147 -0
- package/dist/{native-runtime-DjBHRg5m.mjs → native-runtime-DBzRnEKS.mjs} +9 -5
- package/dist/{ndjson-CDY9IkVC.mjs → ndjson-PclOIwVY.mjs} +2 -2
- package/dist/offload-worker.d.mts +122 -0
- package/dist/offload-worker.mjs +606 -0
- package/dist/{ownership-DyYDF5ww.mjs → ownership-DiA16AsN.mjs} +3 -2
- package/dist/ownership-hCkPzGmr.mjs +571 -0
- package/dist/{page-BHUzTDFt.mjs → page-QF_Ke10s.mjs} +7 -2
- package/dist/{ports-BguS83ex.mjs → ports-R2k4Knz3.mjs} +3 -3
- package/dist/prebuild-D2fsca_Z.mjs +213 -0
- package/dist/preview-DhVbvPAZ.mjs +318 -0
- package/dist/process-identity-BmRm1qV0.mjs +12 -0
- package/dist/{project-CWlvEaeN.mjs → project-Du91Ilvc.mjs} +74 -19
- package/dist/pull-request-pxnDCQzn.mjs +322 -0
- package/dist/pull-requests.d.mts +120 -0
- package/dist/pull-requests.mjs +2 -0
- package/dist/recordings-BIXOzVnr.mjs +37 -0
- package/dist/{reload-BiniwsSZ.mjs → reload-rM9jV1fz.mjs} +109 -24
- package/dist/{remote-cache-CcR6o6lI.mjs → remote-cache-C8-V13XN.mjs} +5 -5
- package/dist/run-f4vI6EnK.mjs +119 -0
- package/dist/{server-bare-DGfgyJ5J.mjs → server-bare-CpPGmnmO.mjs} +33 -15
- package/dist/server-expo-CVqO2c35.mjs +2 -0
- package/dist/{server-expo-BVuwQ65v.mjs → server-expo-MC0rHlOC.mjs} +8 -12
- package/dist/settings-BBpMcDbD.mjs +25 -0
- package/dist/{settings-B-fsC-Om.mjs → settings-ZkDzkzvB.mjs} +22 -10
- package/dist/settings-oTSkkNrM.mjs +1439 -0
- package/dist/settings.schema.json +845 -97
- package/dist/{simslim-DrDN4WuV.mjs → simslim-B8-7AoTI.mjs} +8 -132
- package/dist/{slot-launch-D5t8EiMf.mjs → slot-launch-B8VrHxn1.mjs} +11 -8
- package/dist/{spawn-claims-Drw_o-Fb.mjs → spawn-claims-D0XfSe8u.mjs} +1 -1
- package/dist/spawn-entry-jw19m8Nl.mjs +17 -0
- package/dist/stage-CzaiwFsI.mjs +143 -0
- package/dist/start-CX8Ig-U2.mjs +2 -0
- package/dist/{start-s4RXp1wy.mjs → start-ClDfwXa4.mjs} +116 -65
- package/dist/state-3DwSQIBK.mjs +41 -0
- package/dist/state-B4PQFXMW.d.mts +12 -0
- package/dist/{state-Dk9RVXI7.mjs → state-C1jB7Q8q.mjs} +70 -8
- package/dist/{state-DHqvsbqC.mjs → state-DcW6vOd_.mjs} +7 -4
- package/dist/stats-CEZ_i5je.mjs +77 -0
- package/dist/{status-BgAuxESg.mjs → status-BuvBFeII.mjs} +118 -20
- package/dist/{status-BsPAnfOI.mjs → status-Cs7VLg9D.mjs} +542 -283
- package/dist/status-DlKvVJBE.mjs +49 -0
- package/dist/stdout-reader-C8yjre1x.mjs +23 -0
- package/dist/stim-desktop-Cnz8CPnl.mjs +63 -0
- package/dist/stim-footprint.swift +32 -0
- package/dist/{stim-installations-CINXy_su.mjs → stim-installations-QMEI32v1.mjs} +1 -1
- package/dist/stop-B0Yxtmiy.mjs +48 -0
- package/dist/{stop-BzreOWJp.mjs → stop-Bffozsac.mjs} +83 -16
- package/dist/{stop-BWIF_6BK.mjs → stop-Dt_dM16A.mjs} +2 -2
- package/dist/stop-cause-CidxIqup.mjs +97 -0
- package/dist/supervisor-run.d.mts +43 -11
- package/dist/supervisor-run.mjs +363 -24
- package/dist/support-CAP8qDUF.mjs +316 -0
- package/dist/support-D4aYJniO.mjs +201 -0
- package/dist/teardown-CL6vDs2Y.mjs +727 -0
- package/dist/{xcode-Cp5KvK9x.mjs → toolchain-DkUflduf.mjs} +364 -14
- package/dist/trigger-Bemu8ytw.mjs +65 -0
- package/dist/trigger-CXts6qt1.mjs +2 -0
- package/dist/warm-progress-CpGv3Wb6.mjs +732 -0
- package/dist/{web-BsQs1WlO.mjs → web-DWUaMp8P.mjs} +37 -21
- package/dist/web-run.mjs +310 -13
- package/dist/workspace-process-lock-BL7Q0HCO.mjs +57 -0
- package/dist/workspace-state-B8-aJ1_5.mjs +371 -0
- package/dist/workspaces-BV4pdHFz.mjs +306 -0
- package/dist/{worktree-UrQWXSeb.mjs → worktree-K-xj9Db0.mjs} +625 -256
- package/dist/worktree-jnG8Iph5.mjs +2 -0
- package/package.json +10 -5
- package/shim/bundle-response.cjs +28 -12
- package/dist/activity-CYVDcrt3.mjs +0 -356
- package/dist/budget-CGDkhvv_.mjs +0 -1745
- package/dist/build-progress-DxKpwnaS.mjs +0 -247
- package/dist/idle-DXtx3RbX.mjs +0 -191
- package/dist/in-use-DlqBDKqY.mjs +0 -249
- package/dist/logs-Civ4gbBH.mjs +0 -486
- package/dist/merge-state-ytFQCeh4.mjs +0 -262
- package/dist/server-expo-DIHDhvGg.mjs +0 -2
- package/dist/settings-D61iNOsB.mjs +0 -629
- package/dist/start-Cetfvy8l.mjs +0 -2
- package/dist/stats-CFpFGlJV.mjs +0 -62
- package/dist/stats-CX1Z8Tca.mjs +0 -368
- package/dist/stim-desktop-CXt8-Z3J.mjs +0 -40
- package/dist/stop-cause-hjGBOZTG.mjs +0 -1231
- package/dist/worktree-BVrHsYZG.mjs +0 -752
- package/dist/worktree-CHWWvm5v.mjs +0 -2
- package/dist/{metro-store-DzeWkfqW.mjs → metro-store-CDDZHtPl.mjs} +1 -1
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
import { t as getExecutor } from "./exec-CaErVv7t.mjs";
|
|
2
|
+
import { D as withConfigLock, H as projectDeviceSlots, _ as loadConfig, k as acquireAvdClaim, p as getProject, z as deviceSlotPlatforms } from "./config-D0DbpZtM.mjs";
|
|
3
|
+
import { n as ownership_claim_exports, t as process_identity_exports } from "./process-identity-BmRm1qV0.mjs";
|
|
4
|
+
import { l as phaseLine } from "./command-output-DnJMdEY-.mjs";
|
|
5
|
+
import { gt as repoRoot } from "./settings-oTSkkNrM.mjs";
|
|
6
|
+
import { u as signalProcessTree } from "./metro-BeK1ZPz1.mjs";
|
|
7
|
+
import { g as workspaceAgentDeviceDir, v as workspaceDir } from "./workspace-state-B8-aJ1_5.mjs";
|
|
8
|
+
import { _ as removeSingletonFiles, c as webClaimRoot, f as webProfileDir, g as liveProfileHolder, h as chromeProcessState, i as clearWebRecord, o as readWebRecord, t as CDP_PORT_LABEL } from "./state-C1jB7Q8q.mjs";
|
|
9
|
+
import { t as agentDeviceDaemon, u as readAgentDeviceRecords } from "./activity-BxwdD81S.mjs";
|
|
10
|
+
import { n as readCreatedDevices, r as recordCreatedDevice, t as forgetCreatedDevice } from "./created-devices-D5owjN4C.mjs";
|
|
11
|
+
import { B as ownedAvdSystemImage, I as ownedAvdDirectory, J as resolveOwnedAvdSerial, L as ownedAvdMatchesConfiguration, M as listOrphanedAvdDirectories, O as listAdbDevices, X as shutdownAndroidEmulator, Z as sleepSync, _ as avdStorageRoots, dt as parkSim, et as waitForAndroidEmulatorShutdown, f as assertOwnedAvdStopped, h as avdPathExists, it as isStimOwnedAvd, lt as eraseParkedAfter, mt as removeParkedAfter, nt as wipeAvdUserData, pt as readParked, x as deleteAvd } from "./android-B_NumVyQ.mjs";
|
|
12
|
+
import { E as shutdownIosSim, _ as parseRuntimeVersion, a as deleteIosSim, b as renameIosSim, f as listIosDeviceTypes, g as parkedSimName, o as deleteParkedIosSim, s as eraseIosSim, w as resolveOwnedIosSim } from "./ios-BaInn3sW.mjs";
|
|
13
|
+
import { t as withWorkspaceProcessLock } from "./workspace-process-lock-BL7Q0HCO.mjs";
|
|
14
|
+
import { r as releaseBrowserPort } from "./named-ports-Bk_YXEmE.mjs";
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
16
|
+
import { existsSync, lstatSync, realpathSync, renameSync, rmSync } from "node:fs";
|
|
17
|
+
import { basename, dirname, isAbsolute, join, relative, sep } from "node:path";
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
import { isDeepStrictEqual } from "node:util";
|
|
20
|
+
//#region src/devices/agent-device-cleanup.ts
|
|
21
|
+
function parseAgentDeviceSessions(output) {
|
|
22
|
+
const result = JSON.parse(output);
|
|
23
|
+
if (result?.success !== true || !Array.isArray(result.data?.sessions)) throw new Error("agent-device did not return a successful session list");
|
|
24
|
+
return result.data.sessions.flatMap((entry) => {
|
|
25
|
+
if (!entry || typeof entry !== "object") return [];
|
|
26
|
+
const row = entry;
|
|
27
|
+
const id = row.platform === "ios" ? row.device_udid : row.id;
|
|
28
|
+
if (row.platform !== "ios" && row.platform !== "android" || typeof row.name !== "string" || !row.name.trim() || typeof id !== "string" || !id || typeof row.createdAt !== "number" || !Number.isFinite(row.createdAt) || row.platform === "ios" && row.id !== void 0 && row.id !== id) return [];
|
|
29
|
+
return [{
|
|
30
|
+
name: typeof row.address === "string" && row.address.trim() ? row.address : row.name,
|
|
31
|
+
platform: row.platform,
|
|
32
|
+
id,
|
|
33
|
+
device: typeof row.device === "string" ? row.device : null,
|
|
34
|
+
createdAt: row.createdAt
|
|
35
|
+
}];
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function isOwnDeviceSession(session, device, owner) {
|
|
39
|
+
if (session.platform !== device.platform || device.id !== null && session.id !== device.id) return false;
|
|
40
|
+
if (device.platform === "android" && session.device !== device.avdName.replaceAll("_", " ")) return false;
|
|
41
|
+
if (!owner) return true;
|
|
42
|
+
return owner.claims.some((claim) => claim.deviceId === session.id && claim.session === session.name && claim.workspace !== null && (within(owner.workspace, claim.workspace) || claim.workspace === owner.repoRoot));
|
|
43
|
+
}
|
|
44
|
+
function within(root, path) {
|
|
45
|
+
const rest = relative(root, path);
|
|
46
|
+
return rest === "" || !isAbsolute(rest) && rest.split(sep)[0] !== "..";
|
|
47
|
+
}
|
|
48
|
+
function canonical(path) {
|
|
49
|
+
try {
|
|
50
|
+
return realpathSync(path);
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function deviceClaims(device) {
|
|
56
|
+
return readAgentDeviceRecords(homedir()).filter((record) => record.kind === "claim" && (device.id === null || record.deviceId === device.id)).map((record) => ({
|
|
57
|
+
deviceId: record.deviceId,
|
|
58
|
+
session: record.session,
|
|
59
|
+
workspace: record.workspace && canonical(record.workspace)
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
function report(message) {
|
|
63
|
+
process.stderr.write(`${phaseLine("device", message)}\n`);
|
|
64
|
+
}
|
|
65
|
+
function closeOwnedDeviceSessions(device, stillOwned, workspace, repoRoot) {
|
|
66
|
+
const exec = getExecutor();
|
|
67
|
+
try {
|
|
68
|
+
if (!exec.findExecutable("agent-device")) return;
|
|
69
|
+
const root = workspace === void 0 ? void 0 : canonical(workspace);
|
|
70
|
+
if (root === null) return;
|
|
71
|
+
const owner = root === void 0 ? void 0 : {
|
|
72
|
+
workspace: root,
|
|
73
|
+
repoRoot: repoRoot && (canonical(repoRoot) ?? void 0),
|
|
74
|
+
claims: deviceClaims(device)
|
|
75
|
+
};
|
|
76
|
+
const stateDir = root === void 0 ? void 0 : workspaceAgentDeviceDir(root);
|
|
77
|
+
const stateDirs = [void 0, ...stateDir && !["none", "dead"].includes(agentDeviceDaemon(stateDir)) ? [stateDir] : []];
|
|
78
|
+
for (const dir of stateDirs) {
|
|
79
|
+
const deadline = Date.now() + 15e3;
|
|
80
|
+
const run = (args) => {
|
|
81
|
+
const remaining = deadline - Date.now();
|
|
82
|
+
if (remaining <= 0) throw new Error("agent-device cleanup timed out");
|
|
83
|
+
return exec.runFile("agent-device", [
|
|
84
|
+
...args,
|
|
85
|
+
...dir ? ["--state-dir", dir] : [],
|
|
86
|
+
"--json",
|
|
87
|
+
"--daemon-transport",
|
|
88
|
+
"socket"
|
|
89
|
+
], {
|
|
90
|
+
timeoutMs: Math.min(5e3, remaining),
|
|
91
|
+
killSignal: "SIGKILL"
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
const list = () => parseAgentDeviceSessions(run([
|
|
95
|
+
"session",
|
|
96
|
+
"list",
|
|
97
|
+
"--session",
|
|
98
|
+
"stim-teardown-inventory"
|
|
99
|
+
]));
|
|
100
|
+
try {
|
|
101
|
+
const sessions = list().filter((session) => isOwnDeviceSession(session, device, owner));
|
|
102
|
+
for (const session of sessions) try {
|
|
103
|
+
if (!list().some((current) => current.name === session.name && current.platform === session.platform && current.id === session.id && current.createdAt === session.createdAt)) continue;
|
|
104
|
+
if (!stillOwned(session.id)) continue;
|
|
105
|
+
if (JSON.parse(run([
|
|
106
|
+
"close",
|
|
107
|
+
"--session",
|
|
108
|
+
session.name,
|
|
109
|
+
"--session-lock",
|
|
110
|
+
"reject",
|
|
111
|
+
"--platform",
|
|
112
|
+
device.platform,
|
|
113
|
+
device.platform === "ios" ? "--udid" : "--serial",
|
|
114
|
+
session.id
|
|
115
|
+
]))?.success !== true) throw new Error("agent-device did not confirm session close");
|
|
116
|
+
report(`closed agent-device session ${session.name} on ${session.id}`);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
report(`could not close agent-device session ${session.name} on ${session.id}: ${error.message}`);
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
report(`could not list agent-device sessions for ${device.platform === "ios" ? device.id : device.id ?? device.avdName}: ${error.message}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} catch (error) {
|
|
125
|
+
report(`could not list agent-device sessions for ${device.platform === "ios" ? device.id : device.id ?? device.avdName}: ${error.message}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region src/devices/teardown.ts
|
|
130
|
+
function teardownParkedAvd(avdName) {
|
|
131
|
+
try {
|
|
132
|
+
return removeParkedAfter("android", avdName, () => {
|
|
133
|
+
const result = teardownOwnedAvd(avdName, {
|
|
134
|
+
del: true,
|
|
135
|
+
owner: { pool: true }
|
|
136
|
+
});
|
|
137
|
+
if (result.status !== "torn-down" && result.status !== "missing") throw new Error(result.reason);
|
|
138
|
+
}) ? {
|
|
139
|
+
status: "torn-down",
|
|
140
|
+
label: avdName
|
|
141
|
+
} : {
|
|
142
|
+
status: "skipped",
|
|
143
|
+
kind: "not-parked",
|
|
144
|
+
reason: "emulator is no longer parked"
|
|
145
|
+
};
|
|
146
|
+
} catch (error) {
|
|
147
|
+
return {
|
|
148
|
+
status: "failed",
|
|
149
|
+
reason: String(error?.message || error)
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function teardownParkedIosSim(udid, { label, deleteSim = deleteParkedIosSim } = {}) {
|
|
154
|
+
try {
|
|
155
|
+
const removed = removeParkedAfter("ios", udid, () => {
|
|
156
|
+
closeOwnedDeviceSessions({
|
|
157
|
+
platform: "ios",
|
|
158
|
+
id: udid
|
|
159
|
+
}, () => Boolean(resolveOwnedIosSim(udid).sim));
|
|
160
|
+
deleteSim(udid);
|
|
161
|
+
});
|
|
162
|
+
if (!removed) return {
|
|
163
|
+
status: "skipped",
|
|
164
|
+
kind: "not-parked",
|
|
165
|
+
reason: "simulator is no longer parked"
|
|
166
|
+
};
|
|
167
|
+
return {
|
|
168
|
+
status: "torn-down",
|
|
169
|
+
label: label ?? removed.name
|
|
170
|
+
};
|
|
171
|
+
} catch (error) {
|
|
172
|
+
return {
|
|
173
|
+
status: "failed",
|
|
174
|
+
reason: String(error?.message || error)
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function eraseParkedIosSim(udid, { label } = {}) {
|
|
179
|
+
try {
|
|
180
|
+
const erased = eraseParkedAfter("ios", udid, (_record, eraseStarted) => {
|
|
181
|
+
closeOwnedDeviceSessions({
|
|
182
|
+
platform: "ios",
|
|
183
|
+
id: udid
|
|
184
|
+
}, () => Boolean(resolveOwnedIosSim(udid).sim));
|
|
185
|
+
const resolved = resolveOwnedIosSim(udid);
|
|
186
|
+
if (resolved.missing) throw new Error(`simulator ${udid} is not on this machine`);
|
|
187
|
+
if (resolved.notOwned) throw new Error(`simulator ${udid} is now named ${JSON.stringify(resolved.notOwned)}`);
|
|
188
|
+
const state = resolved.sim.state;
|
|
189
|
+
if (state !== "Shutdown") throw new Error(`simulator ${udid} is ${state}, not Shutdown; it was kept`);
|
|
190
|
+
eraseStarted();
|
|
191
|
+
eraseIosSim(udid);
|
|
192
|
+
});
|
|
193
|
+
if (!erased) return {
|
|
194
|
+
status: "skipped",
|
|
195
|
+
kind: "not-parked",
|
|
196
|
+
reason: "simulator is no longer parked"
|
|
197
|
+
};
|
|
198
|
+
return {
|
|
199
|
+
status: "torn-down",
|
|
200
|
+
label: label ?? erased.name
|
|
201
|
+
};
|
|
202
|
+
} catch (error) {
|
|
203
|
+
return {
|
|
204
|
+
status: "failed",
|
|
205
|
+
reason: String(error?.message || error)
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function eraseParkedAvd(avdName) {
|
|
210
|
+
try {
|
|
211
|
+
return eraseParkedAfter("android", avdName, (_record, eraseStarted) => {
|
|
212
|
+
if (resolveOwnedAvdSerial(avdName).serial) throw new Error(`AVD ${avdName} is running; it was kept`);
|
|
213
|
+
const result = teardownOwnedAvd(avdName, {
|
|
214
|
+
wipe: true,
|
|
215
|
+
owner: { pool: true },
|
|
216
|
+
onWipeStart: eraseStarted
|
|
217
|
+
});
|
|
218
|
+
if (result.status !== "torn-down") throw new Error(result.reason ?? `AVD ${avdName} is ${result.status}`);
|
|
219
|
+
}) ? {
|
|
220
|
+
status: "torn-down",
|
|
221
|
+
label: avdName
|
|
222
|
+
} : {
|
|
223
|
+
status: "skipped",
|
|
224
|
+
kind: "not-parked",
|
|
225
|
+
reason: "emulator is no longer parked"
|
|
226
|
+
};
|
|
227
|
+
} catch (error) {
|
|
228
|
+
return {
|
|
229
|
+
status: "failed",
|
|
230
|
+
reason: String(error?.message || error)
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const IOS_SHUTDOWN_SETTLE_MS = 15e3;
|
|
235
|
+
const ADB_RELEASE_WAIT_MS = 3e3;
|
|
236
|
+
function repoRootAbove(workspace) {
|
|
237
|
+
if (workspace === void 0 || !getExecutor().findExecutable("agent-device")) return void 0;
|
|
238
|
+
const root = repoRoot(workspace);
|
|
239
|
+
try {
|
|
240
|
+
return root && realpathSync(root) !== realpathSync(workspace) ? root : void 0;
|
|
241
|
+
} catch {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const IOS_SHUTDOWN_POLL_MS = 250;
|
|
246
|
+
const IOS_SHUTDOWN_ATTEMPTS = 2;
|
|
247
|
+
function settleIosSimShutdown(udid, { now = Date.now, sleep = sleepSync }) {
|
|
248
|
+
let resolved = resolveOwnedIosSim(udid);
|
|
249
|
+
for (let attempt = 1;; attempt++) {
|
|
250
|
+
const deadline = now() + IOS_SHUTDOWN_SETTLE_MS;
|
|
251
|
+
while (resolved.sim && resolved.sim.state !== "Shutdown" && now() < deadline) {
|
|
252
|
+
sleep(IOS_SHUTDOWN_POLL_MS);
|
|
253
|
+
resolved = resolveOwnedIosSim(udid);
|
|
254
|
+
}
|
|
255
|
+
if (!resolved.sim || resolved.sim.state === "Shutdown" || attempt === IOS_SHUTDOWN_ATTEMPTS) return resolved;
|
|
256
|
+
shutdownIosSim(udid);
|
|
257
|
+
resolved = resolveOwnedIosSim(udid);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function iosShutdownFailureReason(udid, state) {
|
|
261
|
+
return `simulator ${udid} is still ${state} after ${IOS_SHUTDOWN_ATTEMPTS} shutdown attempts and ${IOS_SHUTDOWN_ATTEMPTS * IOS_SHUTDOWN_SETTLE_MS / 1e3}s of waiting`;
|
|
262
|
+
}
|
|
263
|
+
function parkOwnedIosSim(settled, udid, park) {
|
|
264
|
+
if (settled.missing) throw new Error(`simulator ${udid} disappeared after shutdown`);
|
|
265
|
+
if (settled.notOwned) throw new Error(`simulator ${udid} is now named ${JSON.stringify(settled.notOwned)}`);
|
|
266
|
+
const sim = settled.sim;
|
|
267
|
+
if (sim.state !== "Shutdown") throw new Error(iosShutdownFailureReason(udid, sim.state));
|
|
268
|
+
const model = listIosDeviceTypes().find((d) => d.identifier === sim.deviceTypeIdentifier)?.name ?? null;
|
|
269
|
+
const runtime = parseRuntimeVersion(sim.runtime);
|
|
270
|
+
const name = parkedSimName(sim.udid, {
|
|
271
|
+
model,
|
|
272
|
+
runtime
|
|
273
|
+
});
|
|
274
|
+
renameIosSim(sim.udid, name);
|
|
275
|
+
const record = {
|
|
276
|
+
udid: sim.udid,
|
|
277
|
+
name,
|
|
278
|
+
deviceTypeIdentifier: sim.deviceTypeIdentifier,
|
|
279
|
+
runtimeIdentifier: sim.runtime,
|
|
280
|
+
parkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
281
|
+
simslimManaged: Boolean(park.simslimManaged),
|
|
282
|
+
...park.appId ? { bundleId: park.appId } : {},
|
|
283
|
+
...park.cacheKey ? { cacheKey: park.cacheKey } : {}
|
|
284
|
+
};
|
|
285
|
+
return {
|
|
286
|
+
record,
|
|
287
|
+
evicted: parkSim({
|
|
288
|
+
platform: "ios",
|
|
289
|
+
projectPath: park.projectPath,
|
|
290
|
+
slot: park.slot,
|
|
291
|
+
record,
|
|
292
|
+
max: park.max
|
|
293
|
+
})
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function teardownOwnedIosSim(udid, { del = false, label, park, workspace, shutdownClock = {} } = {}) {
|
|
297
|
+
let parkFallback;
|
|
298
|
+
try {
|
|
299
|
+
const resolved = resolveOwnedIosSim(udid);
|
|
300
|
+
if (resolved.notOwned) return {
|
|
301
|
+
status: "skipped",
|
|
302
|
+
kind: "not-owned",
|
|
303
|
+
reason: `sim "${resolved.notOwned}" is not Stim-owned: Stim has no record of creating it`
|
|
304
|
+
};
|
|
305
|
+
if (resolved.missing) return { status: "missing" };
|
|
306
|
+
closeOwnedDeviceSessions({
|
|
307
|
+
platform: "ios",
|
|
308
|
+
id: udid
|
|
309
|
+
}, () => Boolean(resolveOwnedIosSim(udid).sim), workspace);
|
|
310
|
+
const current = resolveOwnedIosSim(udid);
|
|
311
|
+
if (current.missing) return { status: "missing" };
|
|
312
|
+
if (current.notOwned) return {
|
|
313
|
+
status: "skipped",
|
|
314
|
+
kind: "not-owned",
|
|
315
|
+
reason: "simulator ownership changed before shutdown"
|
|
316
|
+
};
|
|
317
|
+
shutdownIosSim(udid);
|
|
318
|
+
const sim = resolved.sim;
|
|
319
|
+
if (del && park && park.max > 0) try {
|
|
320
|
+
const { record, evicted } = parkOwnedIosSim(settleIosSimShutdown(udid, shutdownClock), udid, park);
|
|
321
|
+
const removed = [];
|
|
322
|
+
const failures = [];
|
|
323
|
+
for (const entry of evicted) {
|
|
324
|
+
const result = teardownParkedIosSim(entry.udid, { label: entry.name });
|
|
325
|
+
if (result.status === "torn-down") removed.push({
|
|
326
|
+
udid: entry.udid,
|
|
327
|
+
name: entry.name
|
|
328
|
+
});
|
|
329
|
+
else if (result.status === "failed") failures.push(`could not delete evicted ${entry.name} (${entry.udid}): ${result.reason}`);
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
status: "torn-down",
|
|
333
|
+
label: label ?? sim.name ?? udid,
|
|
334
|
+
parked: {
|
|
335
|
+
udid: record.udid,
|
|
336
|
+
name: record.name
|
|
337
|
+
},
|
|
338
|
+
evicted: removed,
|
|
339
|
+
...failures.length ? { evictionFailures: failures } : {}
|
|
340
|
+
};
|
|
341
|
+
} catch (e) {
|
|
342
|
+
parkFallback = String(e?.message || e);
|
|
343
|
+
}
|
|
344
|
+
if (del) {
|
|
345
|
+
deleteIosSim(udid);
|
|
346
|
+
return {
|
|
347
|
+
status: "torn-down",
|
|
348
|
+
label: label ?? sim.name ?? udid,
|
|
349
|
+
...parkFallback ? { parkFallback } : {}
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
const settled = settleIosSimShutdown(udid, shutdownClock);
|
|
353
|
+
if (settled.sim && settled.sim.state !== "Shutdown") return {
|
|
354
|
+
status: "failed",
|
|
355
|
+
label: label ?? sim.name ?? udid,
|
|
356
|
+
reason: iosShutdownFailureReason(udid, settled.sim.state)
|
|
357
|
+
};
|
|
358
|
+
const root = sim.state === "Shutdown" ? void 0 : repoRootAbove(workspace);
|
|
359
|
+
if (root && settled.sim) closeOwnedDeviceSessions({
|
|
360
|
+
platform: "ios",
|
|
361
|
+
id: udid
|
|
362
|
+
}, () => resolveOwnedIosSim(udid).sim?.state === "Shutdown", workspace, root);
|
|
363
|
+
return {
|
|
364
|
+
status: "torn-down",
|
|
365
|
+
label: label ?? sim.name ?? udid
|
|
366
|
+
};
|
|
367
|
+
} catch (e) {
|
|
368
|
+
return {
|
|
369
|
+
status: "failed",
|
|
370
|
+
reason: String(e?.message || e),
|
|
371
|
+
...parkFallback ? { parkFallback } : {}
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function assertAvdReferences(avdName, owner) {
|
|
376
|
+
const config = loadConfig();
|
|
377
|
+
if (!config) throw new Error("Cannot verify AVD references without a Stim config; the data was kept.");
|
|
378
|
+
if (owner && "projectPath" in owner && owner.expectedRecord && !isDeepStrictEqual(deviceSlotPlatforms(config.projects[owner.projectPath], owner.slot)?.android, owner.expectedRecord)) throw new Error(`AVD ${avdName} has a changed workspace record; its data was kept.`);
|
|
379
|
+
for (const [projectPath, project] of Object.entries(config.projects)) for (const { slot, platforms } of projectDeviceSlots(project)) {
|
|
380
|
+
if (platforms.android?.avdName?.toLowerCase() !== avdName.toLowerCase()) continue;
|
|
381
|
+
if (!owner || !("projectPath" in owner) || owner.projectPath !== projectPath || (owner.slot ?? "default") !== slot || !platforms.android.owned) throw new Error(`AVD ${avdName} is referenced by ${projectPath} (${slot}); its data was kept.`);
|
|
382
|
+
}
|
|
383
|
+
if (readParked("android", { config }).some((entry) => entry.name.toLowerCase() === avdName.toLowerCase()) && !(owner && "pool" in owner)) throw new Error(`AVD ${avdName} is in the emulator pool; its data was kept.`);
|
|
384
|
+
}
|
|
385
|
+
function removeOrphanedAvdDirectory(candidate, owner) {
|
|
386
|
+
const surveyed = listOrphanedAvdDirectories(candidate.name).find((entry) => entry.directory === candidate.directory);
|
|
387
|
+
if (!surveyed || surveyed.name !== candidate.name || surveyed.dev !== candidate.dev || surveyed.ino !== candidate.ino) throw new Error(`AVD data at ${candidate.directory} changed or is registered; it was kept.`);
|
|
388
|
+
assertOwnedAvdStopped(candidate.name, { resolveDirectory: () => candidate.directory });
|
|
389
|
+
const detached = withConfigLock(() => {
|
|
390
|
+
const current = lstatSync(candidate.directory);
|
|
391
|
+
if (!current.isDirectory() || current.dev !== candidate.dev || current.ino !== candidate.ino) throw new Error(`AVD data at ${candidate.directory} changed or is registered; it was kept.`);
|
|
392
|
+
assertAvdReferences(candidate.name, owner);
|
|
393
|
+
const name = `stim-gc-${randomUUID()}`;
|
|
394
|
+
recordCreatedDevice("android", name);
|
|
395
|
+
const destination = join(dirname(candidate.directory), `${name}.avd`);
|
|
396
|
+
try {
|
|
397
|
+
renameSync(candidate.directory, destination);
|
|
398
|
+
} catch (error) {
|
|
399
|
+
forgetCreatedDevice("android", name);
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
402
|
+
forgetCreatedDevice("android", candidate.name);
|
|
403
|
+
return destination;
|
|
404
|
+
});
|
|
405
|
+
const current = lstatSync(detached);
|
|
406
|
+
if (!current.isDirectory() || current.dev !== candidate.dev || current.ino !== candidate.ino) throw new Error(`Detached AVD data changed at ${detached}; it was kept.`);
|
|
407
|
+
try {
|
|
408
|
+
rmSync(detached, { recursive: true });
|
|
409
|
+
} catch (error) {
|
|
410
|
+
throw new Error(`Could not remove AVD data at ${detached}; retry gc --delete: ${error.message}`, { cause: error });
|
|
411
|
+
}
|
|
412
|
+
forgetCreatedDevice("android", basename(detached, ".avd"));
|
|
413
|
+
}
|
|
414
|
+
function teardownUnregisteredAvd(avdName, del, owner, surveyed) {
|
|
415
|
+
const candidates = surveyed ? [surveyed] : listOrphanedAvdDirectories(avdName);
|
|
416
|
+
if (candidates.length === 0) {
|
|
417
|
+
if (avdStorageRoots().some((root) => avdPathExists(join(root, `${avdName}.ini`)) || avdPathExists(join(root, `${avdName}.avd`)))) throw new Error(`AVD ${avdName} is not listed but its storage remains; it was kept.`);
|
|
418
|
+
return { status: "missing" };
|
|
419
|
+
}
|
|
420
|
+
if (!del) return {
|
|
421
|
+
status: "skipped",
|
|
422
|
+
reason: "AVD registration is missing; its data was kept."
|
|
423
|
+
};
|
|
424
|
+
for (const candidate of candidates) {
|
|
425
|
+
if (candidate.name !== avdName) throw new Error(`AVD data does not match ${avdName}; it was kept.`);
|
|
426
|
+
removeOrphanedAvdDirectory(candidate, owner);
|
|
427
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
status: "torn-down",
|
|
430
|
+
label: avdName
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
function teardownOwnedAvd(avdName, options = {}) {
|
|
434
|
+
let claim;
|
|
435
|
+
try {
|
|
436
|
+
if (!isStimOwnedAvd(avdName)) return {
|
|
437
|
+
status: "skipped",
|
|
438
|
+
kind: "not-owned",
|
|
439
|
+
reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
|
|
440
|
+
};
|
|
441
|
+
claim = acquireAvdClaim(avdName);
|
|
442
|
+
const result = teardownClaimedAvd(avdName, claim, options);
|
|
443
|
+
if (result.status === "torn-down" || result.status === "missing") options.onRemoved?.();
|
|
444
|
+
return result;
|
|
445
|
+
} catch (error) {
|
|
446
|
+
return {
|
|
447
|
+
status: "failed",
|
|
448
|
+
reason: String(error?.message || error)
|
|
449
|
+
};
|
|
450
|
+
} finally {
|
|
451
|
+
(0, ownership_claim_exports.releaseClaim)(claim);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
function serialFree(serial) {
|
|
455
|
+
const adb = listAdbDevices();
|
|
456
|
+
return ![
|
|
457
|
+
...adb.emulators,
|
|
458
|
+
...adb.physical,
|
|
459
|
+
...adb.unhealthy
|
|
460
|
+
].some((entry) => entry.serial === serial);
|
|
461
|
+
}
|
|
462
|
+
function teardownClaimedAvd(avdName, claim, { del = false, wipe = false, park, owner, workspace, orphanedDirectory, onlyIfMissing = false, onWipeStart, waitForShutdown = waitForAndroidEmulatorShutdown, assertStopped = assertOwnedAvdStopped, resolveAvd = resolveOwnedAvdSerial }) {
|
|
463
|
+
let parkFallback;
|
|
464
|
+
try {
|
|
465
|
+
if (del || wipe) withConfigLock(() => assertAvdReferences(avdName, owner));
|
|
466
|
+
const resolved = resolveAvd(avdName);
|
|
467
|
+
if (resolved.notOwned) return {
|
|
468
|
+
status: "skipped",
|
|
469
|
+
kind: "not-owned",
|
|
470
|
+
reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
|
|
471
|
+
};
|
|
472
|
+
if (resolved.missing) {
|
|
473
|
+
if (wipe) throw new Error(`AVD ${avdName} is not registered; its data was kept.`);
|
|
474
|
+
return teardownUnregisteredAvd(avdName, del, owner, orphanedDirectory);
|
|
475
|
+
}
|
|
476
|
+
if (onlyIfMissing) return {
|
|
477
|
+
status: "skipped",
|
|
478
|
+
reason: "AVD registration appeared; its record was kept."
|
|
479
|
+
};
|
|
480
|
+
if (orphanedDirectory) return {
|
|
481
|
+
status: "skipped",
|
|
482
|
+
reason: "AVD registration appeared; its data was kept."
|
|
483
|
+
};
|
|
484
|
+
const serial = resolved.serial;
|
|
485
|
+
const stillStopped = (sessionSerial) => {
|
|
486
|
+
const current = resolveAvd(avdName);
|
|
487
|
+
return !current.notOwned && !current.missing && !current.serial && serialFree(sessionSerial);
|
|
488
|
+
};
|
|
489
|
+
if (serial) {
|
|
490
|
+
const stillOwned = () => {
|
|
491
|
+
const current = resolveAvd(avdName);
|
|
492
|
+
return !current.notOwned && !current.missing && current.serial === serial;
|
|
493
|
+
};
|
|
494
|
+
closeOwnedDeviceSessions({
|
|
495
|
+
platform: "android",
|
|
496
|
+
id: serial,
|
|
497
|
+
avdName
|
|
498
|
+
}, stillOwned, workspace);
|
|
499
|
+
if (!stillOwned()) throw new Error(`Owned AVD ${avdName} changed before shutdown; it was kept.`);
|
|
500
|
+
waitForShutdown(avdName, (timeoutMs) => {
|
|
501
|
+
(0, ownership_claim_exports.markClaimChildPending)(claim);
|
|
502
|
+
try {
|
|
503
|
+
shutdownAndroidEmulator(serial, timeoutMs);
|
|
504
|
+
} finally {
|
|
505
|
+
(0, ownership_claim_exports.clearClaimChild)(claim);
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
const root = repoRootAbove(workspace);
|
|
509
|
+
if (root) {
|
|
510
|
+
const deadline = Date.now() + ADB_RELEASE_WAIT_MS;
|
|
511
|
+
while (!serialFree(serial) && Date.now() < deadline) sleepSync(250);
|
|
512
|
+
closeOwnedDeviceSessions({
|
|
513
|
+
platform: "android",
|
|
514
|
+
id: serial,
|
|
515
|
+
avdName
|
|
516
|
+
}, stillStopped, workspace, root);
|
|
517
|
+
}
|
|
518
|
+
} else {
|
|
519
|
+
closeOwnedDeviceSessions({
|
|
520
|
+
platform: "android",
|
|
521
|
+
id: null,
|
|
522
|
+
avdName
|
|
523
|
+
}, stillStopped, workspace);
|
|
524
|
+
waitForShutdown(avdName, null);
|
|
525
|
+
}
|
|
526
|
+
if (wipe && !del) {
|
|
527
|
+
const current = resolveAvd(avdName);
|
|
528
|
+
if (current.notOwned || current.missing) throw new Error(`AVD ${avdName} changed before its data was wiped.`);
|
|
529
|
+
if (current.serial) throw new Error(`Owned AVD ${avdName} started again before its data was wiped.`);
|
|
530
|
+
assertStopped(avdName);
|
|
531
|
+
withConfigLock(() => assertAvdReferences(avdName, owner));
|
|
532
|
+
const directory = ownedAvdDirectory(avdName);
|
|
533
|
+
if (!directory) throw new Error(`The data directory of AVD ${avdName} could not be resolved.`);
|
|
534
|
+
onWipeStart?.();
|
|
535
|
+
wipeAvdUserData(directory);
|
|
536
|
+
}
|
|
537
|
+
if (del) {
|
|
538
|
+
const current = resolveAvd(avdName);
|
|
539
|
+
if (current.notOwned) return {
|
|
540
|
+
status: "skipped",
|
|
541
|
+
kind: "not-owned",
|
|
542
|
+
reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
|
|
543
|
+
};
|
|
544
|
+
if (current.missing) return teardownUnregisteredAvd(avdName, del, owner);
|
|
545
|
+
if (current.serial) throw new Error(`Owned AVD ${avdName} started again before deletion.`);
|
|
546
|
+
assertStopped(avdName);
|
|
547
|
+
withConfigLock(() => assertAvdReferences(avdName, owner));
|
|
548
|
+
if (park && park.max > 0) try {
|
|
549
|
+
const systemImage = ownedAvdSystemImage(avdName);
|
|
550
|
+
if (!systemImage || !park.configuration || !ownedAvdMatchesConfiguration(avdName, park.configuration)) throw new Error("the AVD has no verified creation configuration");
|
|
551
|
+
const evicted = parkSim({
|
|
552
|
+
platform: "android",
|
|
553
|
+
projectPath: park.projectPath,
|
|
554
|
+
slot: park.slot,
|
|
555
|
+
max: park.max,
|
|
556
|
+
record: {
|
|
557
|
+
udid: avdName,
|
|
558
|
+
name: avdName,
|
|
559
|
+
systemImage,
|
|
560
|
+
configuration: park.configuration,
|
|
561
|
+
parkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
562
|
+
...park.appId ? { packageName: park.appId } : {},
|
|
563
|
+
...park.cacheKey ? { cacheKey: park.cacheKey } : {}
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
const removed = [];
|
|
567
|
+
const failures = [];
|
|
568
|
+
for (const entry of evicted) {
|
|
569
|
+
const result = teardownParkedAvd(entry.name);
|
|
570
|
+
if (result.status === "torn-down") removed.push({
|
|
571
|
+
udid: entry.name,
|
|
572
|
+
name: entry.name,
|
|
573
|
+
platform: "android"
|
|
574
|
+
});
|
|
575
|
+
else if (result.status === "failed") failures.push(`could not delete evicted ${entry.name}: ${result.reason}`);
|
|
576
|
+
}
|
|
577
|
+
return {
|
|
578
|
+
status: "torn-down",
|
|
579
|
+
label: avdName,
|
|
580
|
+
parked: {
|
|
581
|
+
udid: avdName,
|
|
582
|
+
name: avdName,
|
|
583
|
+
platform: "android"
|
|
584
|
+
},
|
|
585
|
+
evicted: removed,
|
|
586
|
+
...failures.length ? { evictionFailures: failures } : {}
|
|
587
|
+
};
|
|
588
|
+
} catch (error) {
|
|
589
|
+
parkFallback = String(error?.message || error);
|
|
590
|
+
}
|
|
591
|
+
const directory = ownedAvdDirectory(avdName);
|
|
592
|
+
const paths = avdStorageRoots().map((root) => join(root, `${avdName}.ini`));
|
|
593
|
+
if (directory) paths.push(directory);
|
|
594
|
+
(0, ownership_claim_exports.markClaimChildPending)(claim);
|
|
595
|
+
try {
|
|
596
|
+
deleteAvd(avdName);
|
|
597
|
+
} finally {
|
|
598
|
+
(0, ownership_claim_exports.clearClaimChild)(claim);
|
|
599
|
+
}
|
|
600
|
+
const remaining = paths.filter(avdPathExists);
|
|
601
|
+
if (remaining.length) throw new Error(`AVD ${avdName} was only partially deleted; data remains at ${remaining.join(", ")}.`);
|
|
602
|
+
}
|
|
603
|
+
return {
|
|
604
|
+
status: "torn-down",
|
|
605
|
+
label: avdName,
|
|
606
|
+
serial: resolved.serial ?? null,
|
|
607
|
+
...parkFallback ? { parkFallback } : {}
|
|
608
|
+
};
|
|
609
|
+
} catch (e) {
|
|
610
|
+
return {
|
|
611
|
+
status: "failed",
|
|
612
|
+
reason: String(e?.message || e)
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
const BROWSER_EXIT_WAIT_MS = 15e3;
|
|
617
|
+
const CHROME_KILL_WAIT_MS = 5e3;
|
|
618
|
+
async function stopOwnedChrome(record) {
|
|
619
|
+
for (const signal of ["SIGTERM", "SIGKILL"]) {
|
|
620
|
+
const state = chromeProcessState(record);
|
|
621
|
+
if (state === "gone") return true;
|
|
622
|
+
if (state === "unknown") return false;
|
|
623
|
+
try {
|
|
624
|
+
signalProcessTree(record.pid, signal, { group: true });
|
|
625
|
+
} catch {}
|
|
626
|
+
const deadline = Date.now() + CHROME_KILL_WAIT_MS;
|
|
627
|
+
while (chromeProcessState(record) !== "gone" && Date.now() < deadline) await sleepAsync(50);
|
|
628
|
+
}
|
|
629
|
+
return chromeProcessState(record) === "gone";
|
|
630
|
+
}
|
|
631
|
+
const sleepAsync = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
632
|
+
/**
|
|
633
|
+
* Closes the workspace's Stim-owned Chrome and its supervisor, after verifying both process identities. With
|
|
634
|
+
* `deleteProfile`, it also deletes the profile directory, only when the created-devices ledger lists it.
|
|
635
|
+
*/
|
|
636
|
+
async function teardownOwnedBrowser(root, options = {}) {
|
|
637
|
+
try {
|
|
638
|
+
return await withWorkspaceProcessLock(workspaceDir(root), "web", () => teardownBrowserHeld(root, options), {
|
|
639
|
+
external: true,
|
|
640
|
+
ownerPurpose: "browser teardown"
|
|
641
|
+
});
|
|
642
|
+
} catch (error) {
|
|
643
|
+
return {
|
|
644
|
+
status: "failed",
|
|
645
|
+
reason: String(error?.message || error)
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
/** {@link teardownOwnedBrowser} for a caller that already holds the {@link BROWSER_LOCK} lock. */
|
|
650
|
+
async function teardownBrowserHeld(root, { deleteProfile = false } = {}) {
|
|
651
|
+
try {
|
|
652
|
+
const record = readWebRecord(root);
|
|
653
|
+
const profile = record?.profile ?? webProfileDir(root);
|
|
654
|
+
if (record) {
|
|
655
|
+
const supervisor = (0, process_identity_exports.inspectProcessIdentity)(record);
|
|
656
|
+
const chrome = record.chromeProcess ? (0, process_identity_exports.inspectProcessIdentity)(record.chromeProcess) : "gone";
|
|
657
|
+
if (supervisor === "unknown" || chrome === "unknown") return {
|
|
658
|
+
status: "skipped",
|
|
659
|
+
kind: "not-verified",
|
|
660
|
+
reason: `the browser supervisor (pid ${record.pid}) or its Chrome could not be verified; its record was kept`
|
|
661
|
+
};
|
|
662
|
+
if (supervisor === "same") {
|
|
663
|
+
try {
|
|
664
|
+
process.kill(record.pid, "SIGTERM");
|
|
665
|
+
} catch {}
|
|
666
|
+
if (!await (0, process_identity_exports.waitForProcessExit)(record, BROWSER_EXIT_WAIT_MS)) return {
|
|
667
|
+
status: "failed",
|
|
668
|
+
reason: `browser supervisor pid ${record.pid} did not exit`
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
if (record.chromeProcess && !await stopOwnedChrome(record.chromeProcess)) return {
|
|
672
|
+
status: "failed",
|
|
673
|
+
reason: `Chrome pid ${record.chromeProcess.pid} did not exit`
|
|
674
|
+
};
|
|
675
|
+
const holder = liveProfileHolder(profile);
|
|
676
|
+
if (holder !== null) return {
|
|
677
|
+
status: "skipped",
|
|
678
|
+
kind: "not-verified",
|
|
679
|
+
reason: `pid ${holder} still holds ${profile}, and the browser record does not name it; it was left running`
|
|
680
|
+
};
|
|
681
|
+
const claims = (0, ownership_claim_exports.clearFreeClaimSet)({
|
|
682
|
+
root: webClaimRoot(root),
|
|
683
|
+
label: "browser supervisor"
|
|
684
|
+
});
|
|
685
|
+
if (claims.status !== "cleared") return {
|
|
686
|
+
status: "skipped",
|
|
687
|
+
kind: "not-verified",
|
|
688
|
+
reason: claims.status === "held" ? `the browser supervisor claim is still held${claims.holder ? ` by pid ${claims.holder.owner.pid}` : ""}` : `the browser supervisor claim could not be cleared (${claims.reason}); remove it with ${(0, ownership_claim_exports.claimRemoveCommand)(webClaimRoot(root))}`
|
|
689
|
+
};
|
|
690
|
+
clearWebRecord(root, record);
|
|
691
|
+
await releaseBrowserPort(root, record.cdpPort);
|
|
692
|
+
} else {
|
|
693
|
+
const holder = liveProfileHolder(profile);
|
|
694
|
+
if (holder !== null) return {
|
|
695
|
+
status: "skipped",
|
|
696
|
+
kind: "not-verified",
|
|
697
|
+
reason: `pid ${holder} holds ${profile} but no browser supervisor is recorded; it was left running`
|
|
698
|
+
};
|
|
699
|
+
const port = getProject(root)?.ports?.[CDP_PORT_LABEL];
|
|
700
|
+
if (typeof port === "number") await releaseBrowserPort(root, port);
|
|
701
|
+
}
|
|
702
|
+
if (existsSync(profile)) removeSingletonFiles(profile);
|
|
703
|
+
if (deleteProfile && existsSync(profile)) {
|
|
704
|
+
if (!readCreatedDevices().web.has(profile)) return {
|
|
705
|
+
status: "skipped",
|
|
706
|
+
kind: "not-owned",
|
|
707
|
+
reason: `${profile} is not in Stim's ledger; it was kept`
|
|
708
|
+
};
|
|
709
|
+
rmSync(profile, {
|
|
710
|
+
recursive: true,
|
|
711
|
+
force: true
|
|
712
|
+
});
|
|
713
|
+
forgetCreatedDevice("web", profile);
|
|
714
|
+
}
|
|
715
|
+
return record ? {
|
|
716
|
+
status: "torn-down",
|
|
717
|
+
label: `Chrome (${profile})`
|
|
718
|
+
} : { status: "missing" };
|
|
719
|
+
} catch (error) {
|
|
720
|
+
return {
|
|
721
|
+
status: "failed",
|
|
722
|
+
reason: String(error?.message || error)
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
//#endregion
|
|
727
|
+
export { teardownOwnedBrowser as a, teardownParkedIosSim as c, teardownOwnedAvd as i, eraseParkedIosSim as n, teardownOwnedIosSim as o, teardownBrowserHeld as r, teardownParkedAvd as s, eraseParkedAvd as t };
|