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.
Files changed (152) hide show
  1. package/README.md +2 -0
  2. package/dist/activity-BxwdD81S.mjs +609 -0
  3. package/dist/agent-device-usage-output-Dpih1Tfa.mjs +411 -0
  4. package/dist/{android-d6rGq1pA.mjs → android-B_NumVyQ.mjs} +169 -81
  5. package/dist/{android-LbPFRWVL.mjs → android-Ift4kVvt.mjs} +517 -413
  6. package/dist/android-cas-8U4DPqeR.mjs +167 -0
  7. package/dist/android-cas-compiler.mjs +2 -2
  8. package/dist/{android-XpvCrV7q.mjs → android-r2rBL8kV.mjs} +2 -2
  9. package/dist/{app-install-BpgSgkJj.mjs → app-install-CKWdbXIX.mjs} +128 -151
  10. package/dist/attempt-Bk2ouQVL.mjs +39 -0
  11. package/dist/budget-DjrURfQn.mjs +2700 -0
  12. package/dist/{build-plan-Dcr-3ie0.mjs → build-plan-DjeSb2Xl.mjs} +192 -653
  13. package/dist/build-slots-rUk77xqh.mjs +132 -0
  14. package/dist/{cache-manifest-C68QQ0ua.mjs → cache-manifest-DnUHBJaU.mjs} +2 -2
  15. package/dist/cache-manifest.mjs +1 -1
  16. package/dist/{chrome-BPCiC3ZX.mjs → chrome-BrcVWQyD.mjs} +1 -1
  17. package/dist/cli-BkYZ_6hy.mjs +51 -0
  18. package/dist/cli.mjs +6 -42
  19. package/dist/client-BTY686R6.mjs +1368 -0
  20. package/dist/collector-run.mjs +9 -195
  21. package/dist/{command-output-DVcpw6eE.mjs → command-output-DnJMdEY-.mjs} +1 -1
  22. package/dist/{config-lvc7d3C2.mjs → config-D0DbpZtM.mjs} +28 -29
  23. package/dist/{created-devices-CllH_8eb.mjs → created-devices-D5owjN4C.mjs} +2 -2
  24. package/dist/{dependency-state-DsT7aKeo.mjs → dependency-state-B6cMXZab.mjs} +2 -2
  25. package/dist/{deps-D9IcKqXS.mjs → deps-urbmfXig.mjs} +3 -3
  26. package/dist/{detached-entry-DJHoca49.mjs → detached-entry-CiTEgMR1.mjs} +1 -1
  27. package/dist/dev-client-CroAIjjj.mjs +43 -0
  28. package/dist/{device-CjuuAJjQ.mjs → device-DZQjF7H5.mjs} +8 -7
  29. package/dist/device-capacity-BHwxNBlu.mjs +136 -0
  30. package/dist/device-host-worker.d.mts +1 -0
  31. package/dist/device-host-worker.mjs +964 -0
  32. package/dist/device-ios-BadmeL2V.mjs +381 -0
  33. package/dist/{device-lease-5c3WrVRm.mjs → device-lease-D-hLqx1b.mjs} +43 -36
  34. package/dist/{device-pool-BiyDyPP-.mjs → device-lease-run-CqjShqWW.mjs} +6 -126
  35. package/dist/device-pool-CUaEWMMm.mjs +125 -0
  36. package/dist/{device-remote-D87zEzaq.mjs → device-remote-D5WVWqOq.mjs} +69 -163
  37. package/dist/{doctor-x9pnU9fq.mjs → doctor-Cimf-gGB.mjs} +241 -103
  38. package/dist/{error-diagnostics-C5HzcwCW.mjs → error-diagnostics-B9L4Ae0x.mjs} +15 -18
  39. package/dist/{exec-DK8KaVHK.mjs → exec-CaErVv7t.mjs} +5 -1
  40. package/dist/{gc-ko6mUAYD.mjs → gc-Bqy4bjEu.mjs} +599 -108
  41. package/dist/{android-cas-iZK3DOCr.mjs → gradle-BsO1MdsH.mjs} +8 -294
  42. package/dist/{guide-6R0VNFqI.mjs → guide-B_uyKTxW.mjs} +2511 -239
  43. package/dist/{guide-status-BZrFPIA_.mjs → guide-status-CjEZMAD4.mjs} +3 -3
  44. package/dist/hosted-client-a-WmqE2L.mjs +197 -0
  45. package/dist/hosted-ios-CIWelIqf.mjs +220 -0
  46. package/dist/hosted-logs-D5g041Q3.mjs +187 -0
  47. package/dist/hosted-macos-BSZyIb3y.mjs +135 -0
  48. package/dist/idle-DYVymG92.mjs +1099 -0
  49. package/dist/idle-shutdown-CTsDnAde.mjs +122 -0
  50. package/dist/in-use-DvBw0Kxo.mjs +130 -0
  51. package/dist/{ios-BwDPrnYQ.mjs → ios-BaInn3sW.mjs} +201 -10
  52. package/dist/ios-Cr1h5gnO.mjs +189 -0
  53. package/dist/{ios-P1f5nm2n.mjs → ios-aq3QoGI3.mjs} +1159 -633
  54. package/dist/{ios-device-BiOsSVzd.mjs → ios-device-Ee7sDXfp.mjs} +1 -1
  55. package/dist/{ios-device-DGglU3E_.mjs → ios-device-FfCN_C8p.mjs} +12 -7
  56. package/dist/ios-state-DS5UpYTH.mjs +48 -0
  57. package/dist/{launch-verify-Deg4tB3Z.mjs → launch-verify-2DBRizjo.mjs} +5 -5
  58. package/dist/logs-CHEVK8zD.mjs +371 -0
  59. package/dist/machines-BIBUdhpj.mjs +265 -0
  60. package/dist/macos-CSYEAFmF.mjs +473 -0
  61. package/dist/macos-run.d.mts +5 -0
  62. package/dist/macos-run.mjs +2 -0
  63. package/dist/maintenance-run.d.mts +4 -0
  64. package/dist/maintenance-run.mjs +254 -0
  65. package/dist/{metro-CcEo8Fl8.mjs → metro-BeK1ZPz1.mjs} +20 -120
  66. package/dist/metro-gateway-BJ-rxQAB.mjs +84 -0
  67. package/dist/{named-ports-Blg8IxOh.mjs → named-ports-Bk_YXEmE.mjs} +6 -59
  68. package/dist/native-run-NaBrRFtQ.mjs +147 -0
  69. package/dist/{native-runtime-DjBHRg5m.mjs → native-runtime-DBzRnEKS.mjs} +9 -5
  70. package/dist/{ndjson-CDY9IkVC.mjs → ndjson-PclOIwVY.mjs} +2 -2
  71. package/dist/offload-worker.d.mts +122 -0
  72. package/dist/offload-worker.mjs +606 -0
  73. package/dist/{ownership-DyYDF5ww.mjs → ownership-DiA16AsN.mjs} +3 -2
  74. package/dist/ownership-hCkPzGmr.mjs +571 -0
  75. package/dist/{page-BHUzTDFt.mjs → page-QF_Ke10s.mjs} +7 -2
  76. package/dist/{ports-BguS83ex.mjs → ports-R2k4Knz3.mjs} +3 -3
  77. package/dist/prebuild-D2fsca_Z.mjs +213 -0
  78. package/dist/preview-DhVbvPAZ.mjs +318 -0
  79. package/dist/process-identity-BmRm1qV0.mjs +12 -0
  80. package/dist/{project-CWlvEaeN.mjs → project-Du91Ilvc.mjs} +74 -19
  81. package/dist/pull-request-pxnDCQzn.mjs +322 -0
  82. package/dist/pull-requests.d.mts +120 -0
  83. package/dist/pull-requests.mjs +2 -0
  84. package/dist/recordings-BIXOzVnr.mjs +37 -0
  85. package/dist/{reload-BiniwsSZ.mjs → reload-rM9jV1fz.mjs} +109 -24
  86. package/dist/{remote-cache-CcR6o6lI.mjs → remote-cache-C8-V13XN.mjs} +5 -5
  87. package/dist/run-f4vI6EnK.mjs +119 -0
  88. package/dist/{server-bare-DGfgyJ5J.mjs → server-bare-CpPGmnmO.mjs} +33 -15
  89. package/dist/server-expo-CVqO2c35.mjs +2 -0
  90. package/dist/{server-expo-BVuwQ65v.mjs → server-expo-MC0rHlOC.mjs} +8 -12
  91. package/dist/settings-BBpMcDbD.mjs +25 -0
  92. package/dist/{settings-B-fsC-Om.mjs → settings-ZkDzkzvB.mjs} +22 -10
  93. package/dist/settings-oTSkkNrM.mjs +1439 -0
  94. package/dist/settings.schema.json +845 -97
  95. package/dist/{simslim-DrDN4WuV.mjs → simslim-B8-7AoTI.mjs} +8 -132
  96. package/dist/{slot-launch-D5t8EiMf.mjs → slot-launch-B8VrHxn1.mjs} +11 -8
  97. package/dist/{spawn-claims-Drw_o-Fb.mjs → spawn-claims-D0XfSe8u.mjs} +1 -1
  98. package/dist/spawn-entry-jw19m8Nl.mjs +17 -0
  99. package/dist/stage-CzaiwFsI.mjs +143 -0
  100. package/dist/start-CX8Ig-U2.mjs +2 -0
  101. package/dist/{start-s4RXp1wy.mjs → start-ClDfwXa4.mjs} +116 -65
  102. package/dist/state-3DwSQIBK.mjs +41 -0
  103. package/dist/state-B4PQFXMW.d.mts +12 -0
  104. package/dist/{state-Dk9RVXI7.mjs → state-C1jB7Q8q.mjs} +70 -8
  105. package/dist/{state-DHqvsbqC.mjs → state-DcW6vOd_.mjs} +7 -4
  106. package/dist/stats-CEZ_i5je.mjs +77 -0
  107. package/dist/{status-BgAuxESg.mjs → status-BuvBFeII.mjs} +118 -20
  108. package/dist/{status-BsPAnfOI.mjs → status-Cs7VLg9D.mjs} +542 -283
  109. package/dist/status-DlKvVJBE.mjs +49 -0
  110. package/dist/stdout-reader-C8yjre1x.mjs +23 -0
  111. package/dist/stim-desktop-Cnz8CPnl.mjs +63 -0
  112. package/dist/stim-footprint.swift +32 -0
  113. package/dist/{stim-installations-CINXy_su.mjs → stim-installations-QMEI32v1.mjs} +1 -1
  114. package/dist/stop-B0Yxtmiy.mjs +48 -0
  115. package/dist/{stop-BzreOWJp.mjs → stop-Bffozsac.mjs} +83 -16
  116. package/dist/{stop-BWIF_6BK.mjs → stop-Dt_dM16A.mjs} +2 -2
  117. package/dist/stop-cause-CidxIqup.mjs +97 -0
  118. package/dist/supervisor-run.d.mts +43 -11
  119. package/dist/supervisor-run.mjs +363 -24
  120. package/dist/support-CAP8qDUF.mjs +316 -0
  121. package/dist/support-D4aYJniO.mjs +201 -0
  122. package/dist/teardown-CL6vDs2Y.mjs +727 -0
  123. package/dist/{xcode-Cp5KvK9x.mjs → toolchain-DkUflduf.mjs} +364 -14
  124. package/dist/trigger-Bemu8ytw.mjs +65 -0
  125. package/dist/trigger-CXts6qt1.mjs +2 -0
  126. package/dist/warm-progress-CpGv3Wb6.mjs +732 -0
  127. package/dist/{web-BsQs1WlO.mjs → web-DWUaMp8P.mjs} +37 -21
  128. package/dist/web-run.mjs +310 -13
  129. package/dist/workspace-process-lock-BL7Q0HCO.mjs +57 -0
  130. package/dist/workspace-state-B8-aJ1_5.mjs +371 -0
  131. package/dist/workspaces-BV4pdHFz.mjs +306 -0
  132. package/dist/{worktree-UrQWXSeb.mjs → worktree-K-xj9Db0.mjs} +625 -256
  133. package/dist/worktree-jnG8Iph5.mjs +2 -0
  134. package/package.json +10 -5
  135. package/shim/bundle-response.cjs +28 -12
  136. package/dist/activity-CYVDcrt3.mjs +0 -356
  137. package/dist/budget-CGDkhvv_.mjs +0 -1745
  138. package/dist/build-progress-DxKpwnaS.mjs +0 -247
  139. package/dist/idle-DXtx3RbX.mjs +0 -191
  140. package/dist/in-use-DlqBDKqY.mjs +0 -249
  141. package/dist/logs-Civ4gbBH.mjs +0 -486
  142. package/dist/merge-state-ytFQCeh4.mjs +0 -262
  143. package/dist/server-expo-DIHDhvGg.mjs +0 -2
  144. package/dist/settings-D61iNOsB.mjs +0 -629
  145. package/dist/start-Cetfvy8l.mjs +0 -2
  146. package/dist/stats-CFpFGlJV.mjs +0 -62
  147. package/dist/stats-CX1Z8Tca.mjs +0 -368
  148. package/dist/stim-desktop-CXt8-Z3J.mjs +0 -40
  149. package/dist/stop-cause-hjGBOZTG.mjs +0 -1231
  150. package/dist/worktree-BVrHsYZG.mjs +0 -752
  151. package/dist/worktree-CHWWvm5v.mjs +0 -2
  152. package/dist/{metro-store-DzeWkfqW.mjs → metro-store-CDDZHtPl.mjs} +1 -1
@@ -1,1231 +0,0 @@
1
- import { A as ownership_claim_exports, B as deviceSlotPlatforms, E as withConfigLock$1, O as process_identity_exports, U as projectDeviceSlots, _ as loadConfig$1, d as getConfigDir, k as acquireAvdClaim, p as getProject } from "./config-lvc7d3C2.mjs";
2
- import { t as getExecutor } from "./exec-DK8KaVHK.mjs";
3
- import { r as createNdjsonWriter } from "./ndjson-CDY9IkVC.mjs";
4
- import { M as workspaceLogsDir, a as pidExists, b as writeWorkspaceState, k as workspaceDir, l as signalProcessTree, p as clearWorkspaceStateKey } from "./metro-CcEo8Fl8.mjs";
5
- import { a as withWorkspaceProcessLock, r as releaseBrowserPort, s as workspaceProcessLockPath } from "./named-ports-Blg8IxOh.mjs";
6
- import { t as createLineReader } from "./process-output-Fpl8PYi8.mjs";
7
- import { l as phaseLine } from "./command-output-DVcpw6eE.mjs";
8
- import { a as readAgentDeviceRecords } from "./activity-CYVDcrt3.mjs";
9
- import { F as ownedAvdDirectory, I as ownedAvdMatchesConfiguration, Q as waitForAndroidEmulatorShutdown, X as sleepSync, Y as shutdownAndroidEmulator, b as deleteAvd, d as assertOwnedAvdStopped, et as wipeAvdUserData, g as avdStorageRoots, j as listOrphanedAvdDirectories, lt as readParked, m as avdPathExists, q as resolveOwnedAvdSerial, st as parkSim, tt as isStimOwnedAvd, ut as removeParkedAfter, z as ownedAvdSystemImage } from "./android-d6rGq1pA.mjs";
10
- import { n as readCreatedDevices$1, r as recordCreatedDevice, t as forgetCreatedDevice } from "./created-devices-CllH_8eb.mjs";
11
- import { a as readWebRecord, d as webProfileDir, g as removeSingletonFiles, h as liveProfileHolder, i as clearWebRecord, m as chromeProcessState, s as webClaimRoot, t as CDP_PORT_LABEL } from "./state-Dk9RVXI7.mjs";
12
- import { S as resolveOwnedIosSim, a as deleteParkedIosSim, d as listIosDeviceTypes, g as parseRuntimeVersion, h as parkedSimName, i as deleteIosSim, o as eraseIosSim, v as renameIosSim, w as shutdownIosSim } from "./ios-BwDPrnYQ.mjs";
13
- import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, rmSync, unlinkSync } from "node:fs";
14
- import { LOG_ROTATE_BYTES } from "@stim-cli/core";
15
- import { DEV_SERVER_STOP_REQUEST_KEY, readDevServerStop, readDevServerStopRequest } from "@stim-cli/core/state";
16
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
17
- import { createHash, randomUUID } from "node:crypto";
18
- import { homedir } from "node:os";
19
- import { isDeepStrictEqual } from "node:util";
20
- import { resolve4 } from "node:dns/promises";
21
- import { request } from "node:https";
22
- //#region src/devices/agent-device-cleanup.ts
23
- function parseAgentDeviceSessions(output) {
24
- const result = JSON.parse(output);
25
- if (result?.success !== true || !Array.isArray(result.data?.sessions)) throw new Error("agent-device did not return a successful session list");
26
- return result.data.sessions.flatMap((entry) => {
27
- if (!entry || typeof entry !== "object") return [];
28
- const row = entry;
29
- const id = row.platform === "ios" ? row.device_udid : row.id;
30
- 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 [];
31
- return [{
32
- name: typeof row.address === "string" && row.address.trim() ? row.address : row.name,
33
- platform: row.platform,
34
- id,
35
- device: typeof row.device === "string" ? row.device : null,
36
- createdAt: row.createdAt
37
- }];
38
- });
39
- }
40
- function isOwnDeviceSession(session, device, owner) {
41
- if (session.platform !== device.platform || session.id !== device.id) return false;
42
- if (device.platform === "android" && session.device !== device.avdName.replaceAll("_", " ")) return false;
43
- if (!owner) return true;
44
- return owner.claims.some((claim) => claim.session === session.name && claim.workspace !== null && within(owner.workspace, claim.workspace));
45
- }
46
- function within(root, path) {
47
- const rest = relative(root, path);
48
- return rest === "" || !isAbsolute(rest) && rest.split(sep)[0] !== "..";
49
- }
50
- function canonical(path) {
51
- try {
52
- return realpathSync(path);
53
- } catch {
54
- return null;
55
- }
56
- }
57
- function deviceClaims(device) {
58
- return readAgentDeviceRecords(homedir()).filter((record) => record.kind === "claim" && record.deviceId === device.id).map((record) => ({
59
- session: record.session,
60
- workspace: record.workspace && canonical(record.workspace)
61
- }));
62
- }
63
- function report(message) {
64
- process.stderr.write(`${phaseLine("device", message)}\n`);
65
- }
66
- function closeOwnedDeviceSessions(device, stillOwned, workspace) {
67
- const exec = getExecutor();
68
- const deadline = Date.now() + 15e3;
69
- const run = (args) => {
70
- const remaining = deadline - Date.now();
71
- if (remaining <= 0) throw new Error("agent-device cleanup timed out");
72
- return exec.runFile("agent-device", [
73
- ...args,
74
- "--json",
75
- "--daemon-transport",
76
- "socket"
77
- ], {
78
- timeoutMs: Math.min(5e3, remaining),
79
- killSignal: "SIGKILL"
80
- });
81
- };
82
- const list = () => parseAgentDeviceSessions(run([
83
- "session",
84
- "list",
85
- "--session",
86
- "stim-teardown-inventory"
87
- ]));
88
- try {
89
- if (!exec.findExecutable("agent-device")) return;
90
- const root = workspace === void 0 ? void 0 : canonical(workspace);
91
- if (root === null) return;
92
- const owner = root === void 0 ? void 0 : {
93
- workspace: root,
94
- claims: deviceClaims(device)
95
- };
96
- const sessions = list().filter((session) => isOwnDeviceSession(session, device, owner));
97
- for (const session of sessions) try {
98
- if (!list().some((current) => current.name === session.name && current.platform === session.platform && current.id === session.id && current.createdAt === session.createdAt)) continue;
99
- if (!stillOwned()) return;
100
- if (JSON.parse(run([
101
- "close",
102
- "--session",
103
- session.name,
104
- "--session-lock",
105
- "reject",
106
- "--platform",
107
- device.platform,
108
- device.platform === "ios" ? "--udid" : "--serial",
109
- device.id
110
- ]))?.success !== true) throw new Error("agent-device did not confirm session close");
111
- report(`closed agent-device session ${session.name} on ${device.id}`);
112
- } catch (error) {
113
- report(`could not close agent-device session ${session.name} on ${device.id}: ${error.message}`);
114
- }
115
- } catch (error) {
116
- report(`could not list agent-device sessions for ${device.id}: ${error.message}`);
117
- }
118
- }
119
- //#endregion
120
- //#region src/devices/teardown.ts
121
- function teardownParkedAvd(avdName) {
122
- try {
123
- return removeParkedAfter("android", avdName, () => {
124
- const result = teardownOwnedAvd(avdName, {
125
- del: true,
126
- owner: { pool: true }
127
- });
128
- if (result.status !== "torn-down" && result.status !== "missing") throw new Error(result.reason);
129
- }) ? {
130
- status: "torn-down",
131
- label: avdName
132
- } : {
133
- status: "skipped",
134
- kind: "not-parked",
135
- reason: "emulator is no longer parked"
136
- };
137
- } catch (error) {
138
- return {
139
- status: "failed",
140
- reason: String(error?.message || error)
141
- };
142
- }
143
- }
144
- function teardownParkedIosSim(udid, { label, deleteSim = deleteParkedIosSim } = {}) {
145
- try {
146
- const removed = removeParkedAfter("ios", udid, () => {
147
- closeOwnedDeviceSessions({
148
- platform: "ios",
149
- id: udid
150
- }, () => Boolean(resolveOwnedIosSim(udid).sim));
151
- deleteSim(udid);
152
- });
153
- if (!removed) return {
154
- status: "skipped",
155
- kind: "not-parked",
156
- reason: "simulator is no longer parked"
157
- };
158
- return {
159
- status: "torn-down",
160
- label: label ?? removed.name
161
- };
162
- } catch (error) {
163
- return {
164
- status: "failed",
165
- reason: String(error?.message || error)
166
- };
167
- }
168
- }
169
- const IOS_SHUTDOWN_SETTLE_MS = 15e3;
170
- const IOS_SHUTDOWN_POLL_MS = 250;
171
- const IOS_SHUTDOWN_ATTEMPTS = 2;
172
- function settleIosSimShutdown(udid, { now = Date.now, sleep = sleepSync }) {
173
- let resolved = resolveOwnedIosSim(udid);
174
- for (let attempt = 1;; attempt++) {
175
- const deadline = now() + IOS_SHUTDOWN_SETTLE_MS;
176
- while (resolved.sim && resolved.sim.state !== "Shutdown" && now() < deadline) {
177
- sleep(IOS_SHUTDOWN_POLL_MS);
178
- resolved = resolveOwnedIosSim(udid);
179
- }
180
- if (!resolved.sim || resolved.sim.state === "Shutdown" || attempt === IOS_SHUTDOWN_ATTEMPTS) return resolved;
181
- shutdownIosSim(udid);
182
- resolved = resolveOwnedIosSim(udid);
183
- }
184
- }
185
- function iosShutdownFailureReason(udid, state) {
186
- return `simulator ${udid} is still ${state} after ${IOS_SHUTDOWN_ATTEMPTS} shutdown attempts and ${IOS_SHUTDOWN_ATTEMPTS * IOS_SHUTDOWN_SETTLE_MS / 1e3}s of waiting`;
187
- }
188
- function parkOwnedIosSim(settled, udid, park) {
189
- if (settled.missing) throw new Error(`simulator ${udid} disappeared after shutdown`);
190
- if (settled.notOwned) throw new Error(`simulator ${udid} is now named ${JSON.stringify(settled.notOwned)}`);
191
- const sim = settled.sim;
192
- if (sim.state !== "Shutdown") throw new Error(iosShutdownFailureReason(udid, sim.state));
193
- const model = listIosDeviceTypes().find((d) => d.identifier === sim.deviceTypeIdentifier)?.name ?? null;
194
- const runtime = parseRuntimeVersion(sim.runtime);
195
- eraseIosSim(sim.udid);
196
- const name = parkedSimName(sim.udid, {
197
- model,
198
- runtime
199
- });
200
- renameIosSim(sim.udid, name);
201
- const record = {
202
- udid: sim.udid,
203
- name,
204
- deviceTypeIdentifier: sim.deviceTypeIdentifier,
205
- runtimeIdentifier: sim.runtime,
206
- parkedAt: (/* @__PURE__ */ new Date()).toISOString(),
207
- simslimManaged: Boolean(park.simslimManaged)
208
- };
209
- return {
210
- record,
211
- evicted: parkSim({
212
- platform: "ios",
213
- projectPath: park.projectPath,
214
- slot: park.slot,
215
- record,
216
- max: park.max
217
- })
218
- };
219
- }
220
- function teardownOwnedIosSim(udid, { del = false, label, park, workspace, shutdownClock = {} } = {}) {
221
- let parkFallback;
222
- try {
223
- const resolved = resolveOwnedIosSim(udid);
224
- if (resolved.notOwned) return {
225
- status: "skipped",
226
- kind: "not-owned",
227
- reason: `sim "${resolved.notOwned}" is not Stim-owned: Stim has no record of creating it`
228
- };
229
- if (resolved.missing) return { status: "missing" };
230
- closeOwnedDeviceSessions({
231
- platform: "ios",
232
- id: udid
233
- }, () => Boolean(resolveOwnedIosSim(udid).sim), workspace);
234
- const current = resolveOwnedIosSim(udid);
235
- if (current.missing) return { status: "missing" };
236
- if (current.notOwned) return {
237
- status: "skipped",
238
- kind: "not-owned",
239
- reason: "simulator ownership changed before shutdown"
240
- };
241
- shutdownIosSim(udid);
242
- const sim = resolved.sim;
243
- if (del && park && park.max > 0) try {
244
- const { record, evicted } = parkOwnedIosSim(settleIosSimShutdown(udid, shutdownClock), udid, park);
245
- const removed = [];
246
- const failures = [];
247
- for (const entry of evicted) {
248
- const result = teardownParkedIosSim(entry.udid, { label: entry.name });
249
- if (result.status === "torn-down") removed.push({
250
- udid: entry.udid,
251
- name: entry.name
252
- });
253
- else if (result.status === "failed") failures.push(`could not delete evicted ${entry.name} (${entry.udid}): ${result.reason}`);
254
- }
255
- return {
256
- status: "torn-down",
257
- label: label ?? sim.name ?? udid,
258
- parked: {
259
- udid: record.udid,
260
- name: record.name
261
- },
262
- evicted: removed,
263
- ...failures.length ? { evictionFailures: failures } : {}
264
- };
265
- } catch (e) {
266
- parkFallback = String(e?.message || e);
267
- }
268
- if (del) {
269
- deleteIosSim(udid);
270
- return {
271
- status: "torn-down",
272
- label: label ?? sim.name ?? udid,
273
- ...parkFallback ? { parkFallback } : {}
274
- };
275
- }
276
- const settled = settleIosSimShutdown(udid, shutdownClock);
277
- if (settled.sim && settled.sim.state !== "Shutdown") return {
278
- status: "failed",
279
- label: label ?? sim.name ?? udid,
280
- reason: iosShutdownFailureReason(udid, settled.sim.state)
281
- };
282
- return {
283
- status: "torn-down",
284
- label: label ?? sim.name ?? udid
285
- };
286
- } catch (e) {
287
- return {
288
- status: "failed",
289
- reason: String(e?.message || e),
290
- ...parkFallback ? { parkFallback } : {}
291
- };
292
- }
293
- }
294
- function assertAvdReferences(avdName, owner) {
295
- const config = loadConfig$1();
296
- if (!config) throw new Error("Cannot verify AVD references without a Stim config; the data was kept.");
297
- 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.`);
298
- for (const [projectPath, project] of Object.entries(config.projects)) for (const { slot, platforms } of projectDeviceSlots(project)) {
299
- if (platforms.android?.avdName?.toLowerCase() !== avdName.toLowerCase()) continue;
300
- 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.`);
301
- }
302
- 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.`);
303
- }
304
- function removeOrphanedAvdDirectory(candidate, owner) {
305
- const surveyed = listOrphanedAvdDirectories(candidate.name).find((entry) => entry.directory === candidate.directory);
306
- 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.`);
307
- assertOwnedAvdStopped(candidate.name, { resolveDirectory: () => candidate.directory });
308
- const detached = withConfigLock$1(() => {
309
- const current = lstatSync(candidate.directory);
310
- 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.`);
311
- assertAvdReferences(candidate.name, owner);
312
- const name = `stim-gc-${randomUUID()}`;
313
- recordCreatedDevice("android", name);
314
- const destination = join(dirname(candidate.directory), `${name}.avd`);
315
- try {
316
- renameSync(candidate.directory, destination);
317
- } catch (error) {
318
- forgetCreatedDevice("android", name);
319
- throw error;
320
- }
321
- forgetCreatedDevice("android", candidate.name);
322
- return destination;
323
- });
324
- const current = lstatSync(detached);
325
- if (!current.isDirectory() || current.dev !== candidate.dev || current.ino !== candidate.ino) throw new Error(`Detached AVD data changed at ${detached}; it was kept.`);
326
- try {
327
- rmSync(detached, { recursive: true });
328
- } catch (error) {
329
- throw new Error(`Could not remove AVD data at ${detached}; retry gc --delete: ${error.message}`, { cause: error });
330
- }
331
- forgetCreatedDevice("android", basename(detached, ".avd"));
332
- }
333
- function teardownUnregisteredAvd(avdName, del, owner, surveyed) {
334
- const candidates = surveyed ? [surveyed] : listOrphanedAvdDirectories(avdName);
335
- if (candidates.length === 0) {
336
- 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.`);
337
- return { status: "missing" };
338
- }
339
- if (!del) return {
340
- status: "skipped",
341
- reason: "AVD registration is missing; its data was kept."
342
- };
343
- for (const candidate of candidates) {
344
- if (candidate.name !== avdName) throw new Error(`AVD data does not match ${avdName}; it was kept.`);
345
- removeOrphanedAvdDirectory(candidate, owner);
346
- }
347
- return {
348
- status: "torn-down",
349
- label: avdName
350
- };
351
- }
352
- function teardownOwnedAvd(avdName, options = {}) {
353
- let claim;
354
- try {
355
- if (!isStimOwnedAvd(avdName)) return {
356
- status: "skipped",
357
- kind: "not-owned",
358
- reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
359
- };
360
- claim = acquireAvdClaim(avdName);
361
- const result = teardownClaimedAvd(avdName, claim, options);
362
- if (result.status === "torn-down" || result.status === "missing") options.onRemoved?.();
363
- return result;
364
- } catch (error) {
365
- return {
366
- status: "failed",
367
- reason: String(error?.message || error)
368
- };
369
- } finally {
370
- (0, ownership_claim_exports.releaseClaim)(claim);
371
- }
372
- }
373
- function teardownClaimedAvd(avdName, claim, { del = false, park, owner, workspace, orphanedDirectory, onlyIfMissing = false, waitForShutdown = waitForAndroidEmulatorShutdown, assertStopped = assertOwnedAvdStopped, resolveAvd = resolveOwnedAvdSerial }) {
374
- let parkFallback;
375
- try {
376
- if (del) withConfigLock$1(() => assertAvdReferences(avdName, owner));
377
- const resolved = resolveAvd(avdName);
378
- if (resolved.notOwned) return {
379
- status: "skipped",
380
- kind: "not-owned",
381
- reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
382
- };
383
- if (resolved.missing) return teardownUnregisteredAvd(avdName, del, owner, orphanedDirectory);
384
- if (onlyIfMissing) return {
385
- status: "skipped",
386
- reason: "AVD registration appeared; its record was kept."
387
- };
388
- if (orphanedDirectory) return {
389
- status: "skipped",
390
- reason: "AVD registration appeared; its data was kept."
391
- };
392
- const serial = resolved.serial;
393
- if (serial) {
394
- const stillOwned = () => {
395
- const current = resolveAvd(avdName);
396
- return !current.notOwned && !current.missing && current.serial === serial;
397
- };
398
- closeOwnedDeviceSessions({
399
- platform: "android",
400
- id: serial,
401
- avdName
402
- }, stillOwned, workspace);
403
- if (!stillOwned()) throw new Error(`Owned AVD ${avdName} changed before shutdown; it was kept.`);
404
- waitForShutdown(avdName, (timeoutMs) => {
405
- (0, ownership_claim_exports.markClaimChildPending)(claim);
406
- try {
407
- shutdownAndroidEmulator(serial, timeoutMs);
408
- } finally {
409
- (0, ownership_claim_exports.clearClaimChild)(claim);
410
- }
411
- });
412
- } else waitForShutdown(avdName, null);
413
- if (del) {
414
- const current = resolveAvd(avdName);
415
- if (current.notOwned) return {
416
- status: "skipped",
417
- kind: "not-owned",
418
- reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
419
- };
420
- if (current.missing) return teardownUnregisteredAvd(avdName, del, owner);
421
- if (current.serial) throw new Error(`Owned AVD ${avdName} started again before deletion.`);
422
- assertStopped(avdName);
423
- withConfigLock$1(() => assertAvdReferences(avdName, owner));
424
- if (park && park.max > 0) try {
425
- const systemImage = ownedAvdSystemImage(avdName);
426
- if (!systemImage || !park.configuration || !ownedAvdMatchesConfiguration(avdName, park.configuration)) throw new Error("the AVD has no verified creation configuration");
427
- const directory = ownedAvdDirectory(avdName);
428
- if (!directory) throw new Error("the AVD data directory could not be resolved");
429
- wipeAvdUserData(directory);
430
- const evicted = parkSim({
431
- platform: "android",
432
- projectPath: park.projectPath,
433
- slot: park.slot,
434
- max: park.max,
435
- record: {
436
- udid: avdName,
437
- name: avdName,
438
- systemImage,
439
- configuration: park.configuration,
440
- parkedAt: (/* @__PURE__ */ new Date()).toISOString()
441
- }
442
- });
443
- const removed = [];
444
- const failures = [];
445
- for (const entry of evicted) {
446
- const result = teardownParkedAvd(entry.name);
447
- if (result.status === "torn-down") removed.push({
448
- udid: entry.name,
449
- name: entry.name,
450
- platform: "android"
451
- });
452
- else if (result.status === "failed") failures.push(`could not delete evicted ${entry.name}: ${result.reason}`);
453
- }
454
- return {
455
- status: "torn-down",
456
- label: avdName,
457
- parked: {
458
- udid: avdName,
459
- name: avdName,
460
- platform: "android"
461
- },
462
- evicted: removed,
463
- ...failures.length ? { evictionFailures: failures } : {}
464
- };
465
- } catch (error) {
466
- parkFallback = String(error?.message || error);
467
- }
468
- const directory = ownedAvdDirectory(avdName);
469
- const paths = avdStorageRoots().map((root) => join(root, `${avdName}.ini`));
470
- if (directory) paths.push(directory);
471
- (0, ownership_claim_exports.markClaimChildPending)(claim);
472
- try {
473
- deleteAvd(avdName);
474
- } finally {
475
- (0, ownership_claim_exports.clearClaimChild)(claim);
476
- }
477
- const remaining = paths.filter(avdPathExists);
478
- if (remaining.length) throw new Error(`AVD ${avdName} was only partially deleted; data remains at ${remaining.join(", ")}.`);
479
- }
480
- return {
481
- status: "torn-down",
482
- label: avdName,
483
- serial: resolved.serial ?? null,
484
- ...parkFallback ? { parkFallback } : {}
485
- };
486
- } catch (e) {
487
- return {
488
- status: "failed",
489
- reason: String(e?.message || e)
490
- };
491
- }
492
- }
493
- const BROWSER_EXIT_WAIT_MS = 15e3;
494
- const CHROME_KILL_WAIT_MS = 5e3;
495
- async function stopOwnedChrome(record) {
496
- for (const signal of ["SIGTERM", "SIGKILL"]) {
497
- const state = chromeProcessState(record);
498
- if (state === "gone") return true;
499
- if (state === "unknown") return false;
500
- try {
501
- signalProcessTree(record.pid, signal, { group: true });
502
- } catch {}
503
- const deadline = Date.now() + CHROME_KILL_WAIT_MS;
504
- while (chromeProcessState(record) !== "gone" && Date.now() < deadline) await sleepAsync(50);
505
- }
506
- return chromeProcessState(record) === "gone";
507
- }
508
- const sleepAsync = (ms) => new Promise((done) => setTimeout(done, ms));
509
- /**
510
- * Closes the workspace's Stim-owned Chrome and its supervisor, after verifying both process identities. With
511
- * `deleteProfile`, it also deletes the profile directory, only when the created-devices ledger lists it.
512
- */
513
- async function teardownOwnedBrowser(root, options = {}) {
514
- try {
515
- return await withWorkspaceProcessLock(workspaceDir(root), "web", () => teardownBrowserHeld(root, options), {
516
- external: true,
517
- ownerPurpose: "browser teardown"
518
- });
519
- } catch (error) {
520
- return {
521
- status: "failed",
522
- reason: String(error?.message || error)
523
- };
524
- }
525
- }
526
- /** {@link teardownOwnedBrowser} for a caller that already holds the {@link BROWSER_LOCK} lock. */
527
- async function teardownBrowserHeld(root, { deleteProfile = false } = {}) {
528
- try {
529
- const record = readWebRecord(root);
530
- const profile = record?.profile ?? webProfileDir(root);
531
- if (record) {
532
- const supervisor = (0, process_identity_exports.inspectProcessIdentity)(record);
533
- const chrome = record.chromeProcess ? (0, process_identity_exports.inspectProcessIdentity)(record.chromeProcess) : "gone";
534
- if (supervisor === "unknown" || chrome === "unknown") return {
535
- status: "skipped",
536
- kind: "not-verified",
537
- reason: `the browser supervisor (pid ${record.pid}) or its Chrome could not be verified; its record was kept`
538
- };
539
- if (supervisor === "same") {
540
- try {
541
- process.kill(record.pid, "SIGTERM");
542
- } catch {}
543
- if (!await (0, process_identity_exports.waitForProcessExit)(record, BROWSER_EXIT_WAIT_MS)) return {
544
- status: "failed",
545
- reason: `browser supervisor pid ${record.pid} did not exit`
546
- };
547
- }
548
- if (record.chromeProcess && !await stopOwnedChrome(record.chromeProcess)) return {
549
- status: "failed",
550
- reason: `Chrome pid ${record.chromeProcess.pid} did not exit`
551
- };
552
- const holder = liveProfileHolder(profile);
553
- if (holder !== null) return {
554
- status: "skipped",
555
- kind: "not-verified",
556
- reason: `pid ${holder} still holds ${profile}, and the browser record does not name it; it was left running`
557
- };
558
- const claims = (0, ownership_claim_exports.clearFreeClaimSet)({
559
- root: webClaimRoot(root),
560
- label: "browser supervisor"
561
- });
562
- if (claims.status !== "cleared") return {
563
- status: "skipped",
564
- kind: "not-verified",
565
- 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))}`
566
- };
567
- clearWebRecord(root, record);
568
- await releaseBrowserPort(root, record.cdpPort);
569
- } else {
570
- const holder = liveProfileHolder(profile);
571
- if (holder !== null) return {
572
- status: "skipped",
573
- kind: "not-verified",
574
- reason: `pid ${holder} holds ${profile} but no browser supervisor is recorded; it was left running`
575
- };
576
- const port = getProject(root)?.ports?.[CDP_PORT_LABEL];
577
- if (typeof port === "number") await releaseBrowserPort(root, port);
578
- }
579
- if (existsSync(profile)) removeSingletonFiles(profile);
580
- if (deleteProfile && existsSync(profile)) {
581
- if (!readCreatedDevices$1().web.has(profile)) return {
582
- status: "skipped",
583
- kind: "not-owned",
584
- reason: `${profile} is not in Stim's ledger; it was kept`
585
- };
586
- rmSync(profile, {
587
- recursive: true,
588
- force: true
589
- });
590
- forgetCreatedDevice("web", profile);
591
- }
592
- return record ? {
593
- status: "torn-down",
594
- label: `Chrome (${profile})`
595
- } : { status: "missing" };
596
- } catch (error) {
597
- return {
598
- status: "failed",
599
- reason: String(error?.message || error)
600
- };
601
- }
602
- }
603
- //#endregion
604
- //#region src/engine/public-http-probe.ts
605
- const DNS_TIMEOUT_MS = 5e3;
606
- async function resolveAddresses(hostname, signal) {
607
- if (signal.aborted) throw signal.reason;
608
- let timer;
609
- let onAbort;
610
- const unavailable = new Promise((_resolve, reject) => {
611
- onAbort = () => reject(signal.reason);
612
- signal.addEventListener("abort", onAbort, { once: true });
613
- if (signal.aborted) onAbort();
614
- else timer = setTimeout(() => reject(/* @__PURE__ */ new Error("DNS lookup timed out")), DNS_TIMEOUT_MS);
615
- });
616
- try {
617
- return await Promise.race([resolve4(hostname), unavailable]);
618
- } finally {
619
- clearTimeout(timer);
620
- if (onAbort) signal.removeEventListener("abort", onAbort);
621
- }
622
- }
623
- async function probePublicHttp(url, signal) {
624
- try {
625
- return (await fetch(url, {
626
- signal,
627
- redirect: "follow"
628
- })).status;
629
- } catch (error) {
630
- const cause = error?.cause;
631
- if (cause?.code !== "ENOTFOUND" && cause?.code !== "EAI_AGAIN") return null;
632
- }
633
- const hostname = new URL(url).hostname;
634
- if (!hostname.endsWith(".trycloudflare.com")) return null;
635
- let addresses;
636
- try {
637
- addresses = await resolveAddresses(hostname, signal);
638
- } catch {
639
- return null;
640
- }
641
- const firstAddress = addresses[0];
642
- if (!firstAddress) return null;
643
- return new Promise((resolve) => {
644
- const request$1 = request(url, {
645
- signal,
646
- lookup: (_hostname, options, callback) => {
647
- if (options.all) callback(null, addresses.map((address) => ({
648
- address,
649
- family: 4
650
- })));
651
- else callback(null, firstAddress, 4);
652
- }
653
- }, (response) => {
654
- response.resume();
655
- resolve(response.statusCode ?? null);
656
- });
657
- request$1.on("error", () => resolve(null));
658
- request$1.end();
659
- });
660
- }
661
- //#endregion
662
- //#region src/engine/tunnel.ts
663
- function tunnelArgv(provider, port, ngrokUrl, logFile) {
664
- if (provider === "cloudflared") return {
665
- bin: "cloudflared",
666
- args: [
667
- "tunnel",
668
- "--url",
669
- `http://127.0.0.1:${port}`,
670
- ...logFile ? ["--logfile", logFile] : []
671
- ]
672
- };
673
- return {
674
- bin: "ngrok",
675
- args: [
676
- "http",
677
- String(port),
678
- `--log=${logFile || "stdout"}`,
679
- "--log-format=json",
680
- ...ngrokUrl ? ["--url", ngrokUrl] : []
681
- ]
682
- };
683
- }
684
- const CLOUDFLARED_URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/i;
685
- function parseCloudflaredLine(line) {
686
- const match = line.match(CLOUDFLARED_URL_RE);
687
- return match ? match[0] : null;
688
- }
689
- function parseNgrokLine(line) {
690
- let data;
691
- try {
692
- data = JSON.parse(line);
693
- } catch {
694
- return null;
695
- }
696
- if (typeof data !== "object" || data === null) return null;
697
- const url = data.url;
698
- return typeof url === "string" && /^https?:\/\//i.test(url) ? url : null;
699
- }
700
- function parserFor(provider) {
701
- return provider === "cloudflared" ? parseCloudflaredLine : parseNgrokLine;
702
- }
703
- const URL_TIMEOUT_MS = 15e3;
704
- const REACHABLE_TIMEOUT_MS = 24e4;
705
- const REACHABLE_POLL_MS = 2e3;
706
- const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
707
- async function withManagedTunnelLock(root, fn, options = {}) {
708
- return withWorkspaceProcessLock(managedTunnelLockRoot(root), "metro-tunnel", fn, {
709
- ...options,
710
- rejectOwnerPurposes: ["workspace removal"]
711
- });
712
- }
713
- async function withManagedTunnelRemovalLock(root, fn, options = {}) {
714
- return withWorkspaceProcessLock(managedTunnelLockRoot(root), "metro-tunnel", fn, {
715
- ...options,
716
- ownerPurpose: "workspace removal"
717
- });
718
- }
719
- async function withManagedRemoteWorktreeLock(worktreeRoot, fn, options = {}) {
720
- return withWorkspaceProcessLock(managedRemoteWorktreeLockRoot(worktreeRoot), "managed-remote", fn, {
721
- ...options,
722
- external: true,
723
- rejectOwnerPurposes: ["worktree removal"]
724
- });
725
- }
726
- async function withManagedRemoteWorktreeRemovalLock(worktreeRoot, fn, options = {}) {
727
- return withWorkspaceProcessLock(managedRemoteWorktreeLockRoot(worktreeRoot), "managed-remote", fn, {
728
- ...options,
729
- external: true,
730
- ownerPurpose: "worktree removal"
731
- });
732
- }
733
- function managedLockHolders(root) {
734
- const locks = [{
735
- label: "managed tunnel",
736
- path: workspaceProcessLockPath(managedTunnelLockRoot(root), "metro-tunnel", false)
737
- }, {
738
- label: "managed remote",
739
- path: workspaceProcessLockPath(managedRemoteWorktreeLockRoot(root), "managed-remote", true)
740
- }];
741
- const reasons = [];
742
- for (const { label, path } of locks) {
743
- const claims = (0, ownership_claim_exports.readClaimSet)(path);
744
- const holder = claims.live[0];
745
- if (holder) {
746
- const purpose = typeof holder.details.purpose === "string" ? ` for ${holder.details.purpose}` : "";
747
- reasons.push(`the ${label} lock is held${purpose}`);
748
- } else if (claims.unresolved[0]) reasons.push(`the ${label} lock cannot be resolved: ${claims.unresolved[0].reason}`);
749
- }
750
- return reasons;
751
- }
752
- function managedTunnelLockRoot(root) {
753
- const key = createHash("sha256").update(resolve(root)).digest("hex");
754
- return join(getConfigDir(), "process-locks", key);
755
- }
756
- function managedRemoteWorktreeLockRoot(worktreeRoot) {
757
- const key = createHash("sha256").update(resolve(worktreeRoot)).digest("hex");
758
- return join(getConfigDir(), "process-locks", "worktrees", key);
759
- }
760
- async function defaultProbeReachable(url, signal) {
761
- const status = await probePublicHttp(url, signal);
762
- return status !== null && status > 0;
763
- }
764
- function waitForUrl(child, parseLine, timeoutMs) {
765
- return new Promise((resolve) => {
766
- let settled = false;
767
- let timer;
768
- let onOut;
769
- let onErr;
770
- let onError;
771
- let onExit;
772
- const finish = (url, exited = false) => {
773
- if (settled) return;
774
- settled = true;
775
- clearTimeout(timer);
776
- child.stdout?.removeListener("data", onOut);
777
- child.stderr?.removeListener("data", onErr);
778
- child.removeListener("error", onError);
779
- child.removeListener("exit", onExit);
780
- resolve({
781
- url,
782
- exited
783
- });
784
- };
785
- const onLine = (line) => {
786
- if (settled) return;
787
- const url = parseLine(line);
788
- if (url) finish(url);
789
- };
790
- const outReader = createLineReader(onLine);
791
- const errReader = createLineReader(onLine);
792
- onOut = (chunk) => outReader.push(chunk);
793
- onErr = (chunk) => errReader.push(chunk);
794
- onError = () => finish(null);
795
- onExit = () => finish(null, true);
796
- child.stdout?.setEncoding?.("utf-8");
797
- child.stderr?.setEncoding?.("utf-8");
798
- child.stdout?.on("data", onOut);
799
- child.stderr?.on("data", onErr);
800
- child.on("error", onError);
801
- child.on("exit", onExit);
802
- timer = setTimeout(() => finish(null), timeoutMs);
803
- });
804
- }
805
- function waitForFileUrl(child, logFile, parseLine, timeoutMs) {
806
- return new Promise((resolve) => {
807
- let settled = false;
808
- const finish = (url, exited = false) => {
809
- if (settled) return;
810
- settled = true;
811
- clearInterval(interval);
812
- clearTimeout(timer);
813
- child.removeListener("error", onError);
814
- child.removeListener("exit", onExit);
815
- resolve({
816
- url,
817
- exited
818
- });
819
- };
820
- const probe = () => {
821
- try {
822
- for (const line of readFileSync(logFile, "utf-8").split(/\r?\n/)) {
823
- const url = parseLine(line);
824
- if (url) return finish(url);
825
- }
826
- } catch {}
827
- };
828
- const onError = () => finish(null);
829
- const onExit = () => finish(null, true);
830
- child.on("error", onError);
831
- child.on("exit", onExit);
832
- const interval = setInterval(probe, 25);
833
- const timer = setTimeout(() => finish(null), timeoutMs);
834
- probe();
835
- });
836
- }
837
- function createTunnelLogFile(provider) {
838
- const dir = join(getConfigDir(), "tunnel-logs");
839
- mkdirSync(dir, { recursive: true });
840
- const path = join(dir, `${provider}-${randomUUID()}.log`);
841
- closeSync(openSync(path, "wx", 384));
842
- return path;
843
- }
844
- function removeTunnelLogFile(path) {
845
- if (!path) return;
846
- try {
847
- unlinkSync(path);
848
- } catch {}
849
- }
850
- function removeRecordedTunnelLogFile(path) {
851
- if (!path) return;
852
- const dir = resolve(join(getConfigDir(), "tunnel-logs"));
853
- const resolved = resolve(path);
854
- if (!resolved.startsWith(`${dir}${sep}`) || resolve(join(dir, basename(resolved))) !== resolved) return;
855
- removeTunnelLogFile(resolved);
856
- }
857
- async function waitUntilReachable({ url, now, sleep, probe, timeoutMs }) {
858
- const deadline = now() + timeoutMs;
859
- for (;;) {
860
- if (await probe(url, new AbortController().signal).catch(() => false)) return true;
861
- if (now() >= deadline) return false;
862
- await sleep(REACHABLE_POLL_MS);
863
- }
864
- }
865
- const CLEANUP_TIMEOUT_MS = 1e3;
866
- const CLEANUP_POLL_MS = 25;
867
- async function startTunnel({ provider, port, spawnFn = null, urlTimeoutMs = URL_TIMEOUT_MS, reachableTimeoutMs = REACHABLE_TIMEOUT_MS, probeReachable = defaultProbeReachable, now = Date.now, sleep = defaultSleep, ngrokUrl = null, requireReachable = true, cleanupTimeoutMs = CLEANUP_TIMEOUT_MS, isChildAlive = pidExists, readProcessToken = process_identity_exports.captureProcessToken, logFile = null }) {
868
- const spawn = spawnFn || ((cmd, args, opts) => getExecutor().spawn(cmd, args, opts));
869
- const outputFile = logFile || (spawnFn ? null : createTunnelLogFile(provider));
870
- const { bin, args } = tunnelArgv(provider, port, ngrokUrl, outputFile);
871
- let child;
872
- try {
873
- child = spawn(bin, args, {
874
- stdio: outputFile ? [
875
- "ignore",
876
- "ignore",
877
- "ignore"
878
- ] : [
879
- "ignore",
880
- "pipe",
881
- "pipe"
882
- ],
883
- detached: true
884
- });
885
- } catch (err) {
886
- removeTunnelLogFile(outputFile);
887
- return {
888
- failed: true,
889
- reason: `Could not start ${provider}: ${describe(err)}`
890
- };
891
- }
892
- let childExited = false;
893
- child.once("exit", () => {
894
- childExited = true;
895
- });
896
- const cleanup = async (alreadyExited = childExited) => {
897
- const stopped = await terminateChild(child, {
898
- alreadyExited,
899
- timeoutMs: cleanupTimeoutMs,
900
- now,
901
- sleep,
902
- isAlive: isChildAlive
903
- });
904
- if (stopped) removeTunnelLogFile(outputFile);
905
- return stopped ? { status: "stopped" } : {
906
- status: "failed",
907
- reason: `Sent SIGKILL but could not confirm that pid ${child.pid ?? "unknown"} exited.`
908
- };
909
- };
910
- const failAfterCleanup = async (reason, alreadyExited = childExited) => {
911
- const stopped = await cleanup(alreadyExited);
912
- return stopped.status === "stopped" ? {
913
- failed: true,
914
- reason
915
- } : {
916
- failed: true,
917
- reason: `${reason} ${stopped.reason}`,
918
- cleanupFailed: true
919
- };
920
- };
921
- const pid = child.pid;
922
- if (!pid) return failAfterCleanup(`${provider} started but reported no pid.`);
923
- const captureChildIdentity = () => {
924
- try {
925
- return readProcessToken(pid);
926
- } catch {
927
- return null;
928
- }
929
- };
930
- const initialProcessToken = captureChildIdentity();
931
- if (!initialProcessToken) return failAfterCleanup(`${provider} started but its process identity token could not be read.`);
932
- const { url, exited } = outputFile ? await waitForFileUrl(child, outputFile, parserFor(provider), urlTimeoutMs) : await waitForUrl(child, parserFor(provider), urlTimeoutMs);
933
- if (!url) return failAfterCleanup(exited ? `${provider} exited before printing a tunnel URL.` : `${provider} did not print a tunnel URL within ${urlTimeoutMs}ms.`, exited || childExited);
934
- if (!outputFile) resumeChildPipes(child);
935
- if (requireReachable) {
936
- if (!await waitUntilReachable({
937
- url,
938
- now,
939
- sleep,
940
- probe: probeReachable,
941
- timeoutMs: reachableTimeoutMs
942
- })) return failAfterCleanup(`${url} did not become reachable within ${reachableTimeoutMs}ms; ${provider} may still be registering, or the network path is blocked.`);
943
- }
944
- const finalProcessToken = captureChildIdentity();
945
- if (!finalProcessToken || finalProcessToken !== initialProcessToken) return failAfterCleanup(`${provider} process identity changed before its tunnel could be recorded.`);
946
- if (!outputFile) unrefChildPipes(child);
947
- child.unref?.();
948
- if (childExited) return failAfterCleanup(`${provider} exited before its tunnel could be recorded.`, true);
949
- return {
950
- url,
951
- pid,
952
- processToken: initialProcessToken,
953
- logFile: outputFile,
954
- cleanup: () => cleanup()
955
- };
956
- }
957
- async function startTunnelSequence({ providers, port, ngrokUrl = null, requireReachable = true, start = startTunnel }) {
958
- const failures = [];
959
- for (const provider of providers) {
960
- const result = await start({
961
- provider,
962
- port,
963
- ngrokUrl: provider === "ngrok" ? ngrokUrl : null,
964
- requireReachable
965
- });
966
- if (!("failed" in result)) return {
967
- provider,
968
- ...result
969
- };
970
- failures.push(`${provider}: ${result.reason}`);
971
- if (result.cleanupFailed) return {
972
- failed: true,
973
- reason: failures.join(" ")
974
- };
975
- }
976
- return {
977
- failed: true,
978
- reason: failures.join(" ") || "No managed tunnel provider was selected."
979
- };
980
- }
981
- function closeChildPipes(child) {
982
- child.stdout?.destroy?.();
983
- child.stderr?.destroy?.();
984
- }
985
- function unrefChildPipes(child) {
986
- resumeChildPipes(child);
987
- for (const stream of [child.stdout, child.stderr]) stream?.unref?.();
988
- }
989
- function resumeChildPipes(child) {
990
- child.stdout?.resume?.();
991
- child.stderr?.resume?.();
992
- }
993
- async function signalAndWaitForExit(child, signal, { timeoutMs, now, sleep, isAlive, platform }) {
994
- let exited = false;
995
- const onExit = () => {
996
- exited = true;
997
- };
998
- child.once("exit", onExit);
999
- try {
1000
- if (platform === "win32" && child.pid) signalProcessTree(child.pid, signal, { platform });
1001
- child.kill(signal);
1002
- } catch (err) {
1003
- child.removeListener("exit", onExit);
1004
- return isEsrch(err);
1005
- }
1006
- const hasExited = () => exited;
1007
- const deadline = now() + timeoutMs;
1008
- while (!hasExited() && now() < deadline) await sleep(CLEANUP_POLL_MS);
1009
- if (!hasExited() && child.pid && !isAlive(child.pid)) exited = true;
1010
- child.removeListener("exit", onExit);
1011
- return hasExited();
1012
- }
1013
- async function terminateChild(child, { alreadyExited, timeoutMs, now, sleep, isAlive, platform = process.platform }) {
1014
- if (alreadyExited) {
1015
- closeChildPipes(child);
1016
- return true;
1017
- }
1018
- if (await signalAndWaitForExit(child, "SIGTERM", {
1019
- timeoutMs,
1020
- now,
1021
- sleep,
1022
- isAlive,
1023
- platform
1024
- })) {
1025
- closeChildPipes(child);
1026
- return true;
1027
- }
1028
- const killed = await signalAndWaitForExit(child, "SIGKILL", {
1029
- timeoutMs,
1030
- now,
1031
- sleep,
1032
- isAlive,
1033
- platform
1034
- });
1035
- closeChildPipes(child);
1036
- return killed;
1037
- }
1038
- function describe(err) {
1039
- return err?.message || String(err);
1040
- }
1041
- const STOP_TIMEOUT_MS = 5e3;
1042
- const STOP_POLL_MS = 100;
1043
- function isEsrch(err) {
1044
- return err?.code === "ESRCH";
1045
- }
1046
- async function stopTunnel(record, { isAlive = pidExists, inspectIdentity = process_identity_exports.inspectProcessIdentity, platform = process.platform, kill = (pid) => signalProcessTree(pid, "SIGTERM", { platform }), now = Date.now, sleep = defaultSleep, timeoutMs = STOP_TIMEOUT_MS } = {}) {
1047
- const missing = () => {
1048
- removeRecordedTunnelLogFile(record?.logFile);
1049
- return { status: "missing" };
1050
- };
1051
- const pid = record?.pid;
1052
- if (!pid || !isAlive(pid)) return missing();
1053
- if (!record.processToken) return {
1054
- status: "failed",
1055
- reason: `tunnel pid ${pid} has no process identity token, so Stim cannot verify ownership. Keep the record. Inspect the process, stop it with the provider tooling, remove the stale metroTunnel state, and retry.`
1056
- };
1057
- const identity = inspectIdentity(record);
1058
- if (identity === "gone" || identity === "different") return missing();
1059
- if (identity !== "same") return {
1060
- status: "failed",
1061
- reason: `could not verify the process identity for tunnel pid ${pid}; refusing to signal it.`
1062
- };
1063
- try {
1064
- kill(pid);
1065
- } catch (err) {
1066
- return isEsrch(err) ? missing() : {
1067
- status: "failed",
1068
- reason: describe(err)
1069
- };
1070
- }
1071
- const deadline = now() + timeoutMs;
1072
- const exited = () => {
1073
- if (!isAlive(pid)) return true;
1074
- const current = inspectIdentity(record);
1075
- return current === "gone" || current === "different";
1076
- };
1077
- while (now() < deadline && !exited()) await sleep(STOP_POLL_MS);
1078
- if (!exited()) return {
1079
- status: "failed",
1080
- reason: `pid ${pid} did not exit within ${timeoutMs}ms.`
1081
- };
1082
- removeRecordedTunnelLogFile(record.logFile);
1083
- return { status: "stopped" };
1084
- }
1085
- //#endregion
1086
- //#region src/supervisor/ownership.ts
1087
- function resolveSupervisorTarget({ state, record, reservedPort, isAlive = pidExists, inspectIdentity = process_identity_exports.inspectProcessIdentity } = {}) {
1088
- const statePid = numberOrNull(state?.pid);
1089
- const recordPid = numberOrNull(record?.pid);
1090
- const pid = statePid ?? recordPid;
1091
- if (!pid) return { status: "none" };
1092
- if (statePid && recordPid && statePid !== recordPid) return {
1093
- status: "unverified",
1094
- pid,
1095
- reason: `workspace state.json records supervisor pid ${statePid} but the registry records pid ${recordPid}`
1096
- };
1097
- if (!isAlive(pid)) return {
1098
- status: "stale",
1099
- pid
1100
- };
1101
- const port = numberOrNull(state?.port) ?? numberOrNull(record?.port);
1102
- if (reservedPort !== null && reservedPort !== void 0 && port !== null && port !== reservedPort) return {
1103
- status: "unverified",
1104
- pid,
1105
- reason: `supervisor pid ${pid} records port ${port}, but this project reserved port ${reservedPort}`
1106
- };
1107
- if (reservedPort !== null && reservedPort !== void 0 && port === null) return {
1108
- status: "unverified",
1109
- pid,
1110
- reason: `supervisor pid ${pid} has no recorded port, so it cannot be matched against reserved port ${reservedPort}`
1111
- };
1112
- const identityRecord = statePid ? state : record;
1113
- if (statePid && recordPid && state?.processToken !== record?.processToken) return {
1114
- status: "unverified",
1115
- pid,
1116
- reason: "workspace state and registry process identities disagree"
1117
- };
1118
- const identity = inspectIdentity(identityRecord);
1119
- if (identity === "gone" || identity === "different") return {
1120
- status: "stale",
1121
- pid
1122
- };
1123
- if (identity !== "same") return {
1124
- status: "unverified",
1125
- pid,
1126
- reason: "the recorded process identity could not be verified"
1127
- };
1128
- return {
1129
- status: "ours",
1130
- processToken: identityRecord.processToken,
1131
- pid,
1132
- port,
1133
- mode: state?.mode ?? record?.mode ?? null,
1134
- startedAt: state?.startedAt ?? record?.startedAt ?? null
1135
- };
1136
- }
1137
- function numberOrNull(v) {
1138
- return typeof v === "number" && Number.isSafeInteger(v) && v > 0 ? v : null;
1139
- }
1140
- //#endregion
1141
- //#region src/supervisor/stop-cause.ts
1142
- function requestDevServerStop(root, processToken, requester) {
1143
- if (!processToken) return;
1144
- try {
1145
- writeWorkspaceState(root, { [DEV_SERVER_STOP_REQUEST_KEY]: {
1146
- processToken,
1147
- by: requester.by,
1148
- pid: process.pid,
1149
- at: (/* @__PURE__ */ new Date()).toISOString(),
1150
- ...requester.workspace ? { workspace: requester.workspace } : {}
1151
- } });
1152
- } catch {}
1153
- }
1154
- function withdrawDevServerStopRequest(root, processToken) {
1155
- if (!processToken) return;
1156
- try {
1157
- clearWorkspaceStateKey(root, DEV_SERVER_STOP_REQUEST_KEY, (value) => {
1158
- return readDevServerStopRequest({ [DEV_SERVER_STOP_REQUEST_KEY]: value })?.processToken === processToken;
1159
- });
1160
- } catch {}
1161
- }
1162
- function devServerStopRecord(trigger, request, at) {
1163
- if (request) return {
1164
- reason: "requested",
1165
- at,
1166
- by: request.by,
1167
- byPid: request.pid,
1168
- ...request.workspace ? { byWorkspace: request.workspace } : {}
1169
- };
1170
- return trigger.kind === "signal" ? {
1171
- reason: "signal",
1172
- at,
1173
- signal: trigger.signal
1174
- } : {
1175
- reason: "server-exited",
1176
- at,
1177
- mode: trigger.mode,
1178
- code: trigger.code,
1179
- signal: trigger.signal
1180
- };
1181
- }
1182
- function describeDevServerStop(record) {
1183
- if (record.reason === "idle") return `idle for ${record.idleMinutes} minutes (metro.idleStopMinutes)`;
1184
- if (record.reason === "requested") return `stop requested by ${record.by} (pid ${record.byPid}${record.byWorkspace ? ` in ${record.byWorkspace}` : ""})`;
1185
- if (record.reason === "signal") return `received ${record.signal} with no Stim stop request recorded, so it likely came from outside Stim`;
1186
- const detail = record.signal ? `signal ${record.signal}` : `exit code ${record.code ?? "unknown"}`;
1187
- const expoSignal = record.mode === "expo-child" && record.code === 0 ? "; Expo CLI also exits 0 on SIGTERM, and no Stim stop request was recorded" : "";
1188
- return `the ${record.mode} dev server exited unexpectedly (${detail})${expoSignal}`;
1189
- }
1190
- function devServerStopLevel(record) {
1191
- if (record.reason === "server-exited") return "error";
1192
- return record.reason === "signal" ? "warn" : "info";
1193
- }
1194
- /** The log line for a recorded supervisor whose process is proven gone, or null while it may still run. */
1195
- function vanishedSupervisorMessage(record) {
1196
- if (!record || typeof record.pid !== "number") return null;
1197
- const identity = (0, process_identity_exports.inspectProcessIdentity)(record);
1198
- if (identity !== "gone" && identity !== "different") return null;
1199
- const started = typeof record.startedAt === "string" ? ` (started ${record.startedAt})` : "";
1200
- return `supervisor pid ${record.pid}${started} ${VANISHED}`;
1201
- }
1202
- const VANISHED = "is gone and recorded no cause: usually SIGKILL, a crash or a machine restart";
1203
- function logVanishedSupervisor(root, record) {
1204
- const msg = vanishedSupervisorMessage(record);
1205
- if (!msg) return;
1206
- const writer = createNdjsonWriter(join(workspaceLogsDir(root), "metro.ndjson"), { maxBytes: LOG_ROTATE_BYTES });
1207
- writer.write({
1208
- src: "metro",
1209
- level: "warn",
1210
- event: "supervisor_vanished",
1211
- msg
1212
- });
1213
- writer.close();
1214
- }
1215
- /** The recorded stop, else a vanished supervisor when status proved the recorded one gone. */
1216
- function metroLastStop(state, supervisor) {
1217
- const recorded = readDevServerStop(state);
1218
- if (recorded) return recorded;
1219
- if (supervisor?.status !== "stale" || typeof supervisor.pid !== "number") return null;
1220
- const startedAt = typeof supervisor.startedAt === "string" ? supervisor.startedAt : null;
1221
- return {
1222
- reason: "vanished",
1223
- pid: supervisor.pid,
1224
- startedAt
1225
- };
1226
- }
1227
- function describeMetroLastStop(stop) {
1228
- return stop.reason === "vanished" ? `supervisor pid ${stop.pid} ${VANISHED}` : describeDevServerStop(stop);
1229
- }
1230
- //#endregion
1231
- export { teardownOwnedIosSim as C, teardownOwnedBrowser as S, teardownParkedIosSim as T, withManagedTunnelLock as _, logVanishedSupervisor as a, teardownBrowserHeld as b, vanishedSupervisorMessage as c, managedLockHolders as d, startTunnelSequence as f, withManagedRemoteWorktreeRemovalLock as g, withManagedRemoteWorktreeLock as h, devServerStopRecord as i, withdrawDevServerStopRequest as l, terminateChild as m, describeMetroLastStop as n, metroLastStop as o, stopTunnel as p, devServerStopLevel as r, requestDevServerStop as s, describeDevServerStop as t, resolveSupervisorTarget as u, withManagedTunnelRemovalLock as v, teardownParkedAvd as w, teardownOwnedAvd as x, probePublicHttp as y };