stim 1.18.0 → 1.20.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 +21 -4
- package/dist/{activity-BxnkDZDi.mjs → activity-BrGh9EAI.mjs} +3 -3
- package/dist/{agent-device-usage-output-CZ66YA4T.mjs → agent-device-usage-output-DVyv_hWj.mjs} +7 -7
- package/dist/android-0vv-OruW.mjs +6 -0
- package/dist/android-Brx6EKvJ.mjs +1258 -0
- package/dist/{android-DC3ZLuw0.mjs → android-BsYNR81W.mjs} +103 -45
- package/dist/{android-CBwCNicO.mjs → android-CK3fpEyZ.mjs} +2 -2
- package/dist/{android-cas-BBFyr9CA.mjs → android-cas-DCAuXryb.mjs} +9 -5
- package/dist/android-cas-compiler.mjs +1 -1
- package/dist/api-run.mjs +159 -8
- package/dist/{api-D0VtFhqk.mjs → api-zuX3ynYN.mjs} +9 -7
- package/dist/api.d.mts +104 -4
- package/dist/api.mjs +1 -1
- package/dist/{app-install-D-PUENug.mjs → app-install-0LgaQRT7.mjs} +39 -18
- package/dist/artifact-CnC0mfov.mjs +807 -0
- package/dist/artifact-DClXJNWx.mjs +772 -0
- package/dist/artifact-lifecycle-DzPpBYvK.mjs +30 -0
- package/dist/{web-DTU0uiWj.mjs → browser-web-CzBmhmgs.mjs} +136 -174
- package/dist/{budget-DIhL2BKx.mjs → budget-BONq9vXY.mjs} +3096 -3244
- package/dist/build-BixRI6sB.mjs +462 -0
- package/dist/build-plan-CP0tckNU.mjs +553 -0
- package/dist/{cache-manifest-BI7ozfmH.mjs → cache-manifest-qA3dT2x2.mjs} +1 -1
- package/dist/cache-manifest.mjs +1 -1
- package/dist/ccache-CjHQ5F8g.mjs +142 -0
- package/dist/{chrome-DJnmnS5q.mjs → chrome-Dp2lMLBe.mjs} +1 -1
- package/dist/{cli-DNJMwADx.mjs → cli-B2R8wH1F.mjs} +20 -20
- package/dist/cli.mjs +1 -1
- package/dist/{client-DzhapoMB.mjs → client-BLy1Pw2V.mjs} +172 -165
- package/dist/collector-run.d.mts +10 -0
- package/dist/collector-run.mjs +7 -7
- package/dist/{command-output-CBtzyIgF.mjs → command-output-BjPN99ZB.mjs} +2 -2
- package/dist/{config-CgYMahz7.mjs → config-D0LhTY3S.mjs} +13 -13
- package/dist/{created-devices-DKbeWsiK.mjs → created-devices-HU_DAb_Q.mjs} +7 -6
- package/dist/{dependency-state-xKoOypBm.mjs → dependency-state-B2m8eqsS.mjs} +2 -2
- package/dist/{detached-entry-BLfcgc4O.mjs → detached-entry-BXq4fIl-.mjs} +1 -1
- package/dist/{dev-client-CLnIH02x.mjs → dev-client-ClATaJoH.mjs} +2 -2
- package/dist/{device-D7oCHGWi.mjs → device-B7lsC0K1.mjs} +8 -8
- package/dist/device-DVCwAFlX.mjs +727 -0
- package/dist/{device-capacity-EUOOYr8T.mjs → device-capacity-Ca_ke1LE.mjs} +25 -15
- package/dist/device-host-worker.mjs +38 -18
- package/dist/{device-ios-6MewKKnc.mjs → device-ios-CRUqPxKw.mjs} +12 -11
- package/dist/{device-lease-Co-EwifR.mjs → device-lease-DHOBXGgS.mjs} +135 -22
- package/dist/{device-lease-run-w1xhCqIW.mjs → device-lease-run-CvDeHTkG.mjs} +4 -4
- package/dist/{device-pool-F5WvRPtD.mjs → device-pool-L2-5GxMy.mjs} +3 -3
- package/dist/{device-remote-tHbd1fKy.mjs → device-remote-Bmg42063.mjs} +17 -69
- package/dist/doctor-C2MSuMMg.mjs +515 -0
- package/dist/{doctor-B9ckYUoU.mjs → doctor-DPAqhVCK.mjs} +334 -721
- package/dist/doctor-watchman-CB4NnVZm.mjs +260 -0
- package/dist/{simslim-EHBQ2R4G.mjs → eas-build-Dw_ec2Km.mjs} +6 -152
- package/dist/{stim-installations-aR_WsNOA.mjs → eas-session-ledger-DZlC-Fq3.mjs} +57 -4
- package/dist/{error-diagnostics-BRXOqDzG.mjs → error-diagnostics-Du7I_GuY.mjs} +17 -12
- package/dist/errors-DzkMYh8Q.mjs +205 -0
- package/dist/{exec-DWhCZDGm.mjs → exec-B81Qodjn.mjs} +69 -15
- package/dist/{gc-Dh6mXOKC.mjs → gc-PWp34zGb.mjs} +23 -22
- package/dist/{gradle-C6GKnunQ.mjs → gradle-C2OEKCCI.mjs} +50 -125
- package/dist/{guide-tm6igAaa.mjs → guide-Dai9lphv.mjs} +862 -538
- package/dist/{hosted-android-Bv285t_t.mjs → hosted-android-DQ-Agsd8.mjs} +5 -5
- package/dist/{hosted-client-C-rMtppF.mjs → hosted-client-Io01s5By.mjs} +9 -3
- package/dist/{hosted-ios-D5mNmtj4.mjs → hosted-ios-DIxFnYp-.mjs} +3 -3
- package/dist/{hosted-logs-DN8Iz4mJ.mjs → hosted-logs-DP-ABU_M.mjs} +8 -8
- package/dist/{hosted-macos-B-wEjlwG.mjs → hosted-macos-Aziw9T_9.mjs} +7 -6
- package/dist/{hosted-native-DeeOG8xZ.mjs → hosted-native-BdAQi21M.mjs} +23 -16
- package/dist/{idle-shutdown-jjtxAn5C.mjs → idle-shutdown-DdMqoSUB.mjs} +7 -7
- package/dist/ios-BZ9dg2Fr.mjs +9 -0
- package/dist/{ios-B3SbOWdw.mjs → ios-CXLIUxXJ.mjs} +180 -24
- package/dist/ios-CZuv6gRv.mjs +1112 -0
- package/dist/{ios-Bvbf0SGY.mjs → ios-VGvNdsv2.mjs} +1 -1
- package/dist/{ios-device-Bn7jWDB1.mjs → ios-device-B1B14xND.mjs} +1 -1
- package/dist/{ios-device-vOaByMhw.mjs → ios-device-BreZOpM0.mjs} +3 -3
- package/dist/ios-project-B9drt_aY.mjs +197 -0
- package/dist/{ios-state-B2pES6Vs.mjs → ios-state-CjrL2qv-.mjs} +1 -1
- package/dist/js-swap-C8icNYIY.mjs +432 -0
- package/dist/launch-DxH0THLP.mjs +745 -0
- package/dist/launch-YrbqaIFj.mjs +1530 -0
- package/dist/{launch-verify-hskNcWkZ.mjs → launch-verify-BzRdbuO1.mjs} +26 -7
- package/dist/{logs-1Ylc1Ri6.mjs → logs-DHaRNfg7.mjs} +11 -11
- package/dist/macos-C0iljhsN.mjs +2 -0
- package/dist/{macos-DF6C7wUU.mjs → macos-V_3Ui7g4.mjs} +125 -155
- package/dist/macos-run.mjs +1 -1
- package/dist/maintenance-run.mjs +13 -10
- package/dist/{metro-Dh8T4Ytd.mjs → metro-D_j9sN-B.mjs} +209 -43
- package/dist/{metro-gateway-CEUw9RMH.mjs → metro-gateway-BBJldOdi.mjs} +2 -2
- package/dist/miss-reason-BPAzZS0r.mjs +238 -0
- package/dist/{named-ports-BFIz1Shz.mjs → named-ports-C2lvoPsb.mjs} +12 -5
- package/dist/native-android-lHzaew7r.mjs +226 -0
- package/dist/native-gradle-inputs-BQnIdAlL.mjs +356 -0
- package/dist/{native-runtime-D_603JiC.mjs → native-runtime-BRmWL7ru.mjs} +9 -8
- package/dist/native-xcode-inputs-eutS19jU.mjs +307 -0
- package/dist/native-xcode-ios-DP3cHLwu.mjs +434 -0
- package/dist/{ndjson-LGZAMsqa.mjs → ndjson-GOnbyvso.mjs} +6 -6
- package/dist/offload-worker.d.mts +12 -6
- package/dist/offload-worker.mjs +409 -73
- package/dist/{ownership-D6Se7gg_.mjs → ownership-DXzMMSx6.mjs} +2 -2
- package/dist/{placement-log-BcPhRwR4.mjs → placement-log-reFbjqEr.mjs} +7 -3
- package/dist/plan-BDk2AeY_.mjs +260 -0
- package/dist/plan-placement-D_YYlE6N.mjs +73 -0
- package/dist/{ports-BLTtfBq-.mjs → ports-jzvAJXXL.mjs} +3 -3
- package/dist/{prebuild-juCyreaR.mjs → prebuild-Bw44QxVh.mjs} +4 -4
- package/dist/{preview-D5V07Nrl.mjs → preview-BaBJ72Al.mjs} +51 -34
- package/dist/project-B8_duAzW.mjs +1067 -0
- package/dist/project-plan-BPwbLwQu.mjs +10 -0
- package/dist/{pull-request-u4vp7zmK.mjs → pull-request-vOhLNmIb.mjs} +1 -1
- package/dist/pull-requests.mjs +1 -1
- package/dist/react-native-android-chm1DMlA.mjs +921 -0
- package/dist/react-native-build-oAw6gQeW.mjs +239 -0
- package/dist/react-native-doctor-DYXoh-6I.mjs +78 -0
- package/dist/react-native-ios-SCTK_CT5.mjs +595 -0
- package/dist/{recordings-BAWltsVt.mjs → recordings-Bk-VivUV.mjs} +1 -1
- package/dist/{reload-DvyWzVjJ.mjs → reload-D34GkY78.mjs} +17 -14
- package/dist/remote-BU9UyhCJ.mjs +192 -0
- package/dist/{remote-cache-BGg7aGIZ.mjs → remote-cache-Bsts6f6Y.mjs} +5 -5
- package/dist/result-BDoRk1k3.mjs +186 -0
- package/dist/{run-vhtDgpHO.mjs → run-qvff78hv.mjs} +3 -3
- package/dist/{server-bare-B9E7S1aR.mjs → server-bare-dWjAarki.mjs} +6 -6
- package/dist/server-command-7XIKlWtO.mjs +69 -0
- package/dist/{server-expo-DoBHhuzK.mjs → server-expo-CsYu2aKL.mjs} +107 -82
- package/dist/server-expo-DV43vep4.mjs +2 -0
- package/dist/settings-Bbztm4oV.mjs +1503 -0
- package/dist/{settings-Byp2AQ_E.mjs → settings-CT3E71-i.mjs} +6 -7
- package/dist/{state-lKh8shBp.d.mts → settings-vk3mJGSm.d.mts} +4 -2
- package/dist/settings.schema.json +156 -6
- package/dist/simslim-ChMfe8rg.mjs +149 -0
- package/dist/{slot-launch-C1nqh4mu.mjs → slot-launch-B9hn96wd.mjs} +6 -6
- package/dist/{stage-g_WqWeaE.mjs → stage-BNKKdDNc.mjs} +6 -3
- package/dist/start-CbQCSnXQ.mjs +2 -0
- package/dist/{start-D-IeOyoK.mjs → start-CdvcTaXr.mjs} +113 -77
- package/dist/{state-xnqpx3AD.mjs → state-Boo0IMJq.mjs} +4 -2
- package/dist/{state-BG-aAk4k.mjs → state-DI84A-uu.mjs} +29 -10
- package/dist/{stats-DY6ekz8F.mjs → stats-BIIDrjA8.mjs} +6 -6
- package/dist/{status-BEeL6IKz.mjs → status-BDGHwkCs.mjs} +3 -1
- package/dist/{status-DCjZeE-K.mjs → status-DARMkc9q.mjs} +80 -294
- package/dist/status-watch-DvmI3SmN.mjs +296 -0
- package/dist/{stim-desktop-Behws_fi.mjs → stim-desktop-8GQX2OV8.mjs} +1 -1
- package/dist/{stop-DIJtD2GL.mjs → stop-CCZS-C9m.mjs} +2 -2
- package/dist/stop-WQPQHLuy.mjs +72 -0
- package/dist/{stop-BTjs3xbc.mjs → stop-iNemHsJC.mjs} +81 -33
- package/dist/supervisor-run.d.mts +7 -2
- package/dist/supervisor-run.mjs +32 -20
- package/dist/{support-Bwvbrq5-.mjs → support-CnE_SIjt.mjs} +17 -7
- package/dist/{support-BaayeYCf.mjs → support-DCco5fut.mjs} +61 -8
- package/dist/swiftpm-macos-afeNUExW.mjs +119 -0
- package/dist/{native-run-B06FkmZx.mjs → tailnet-CoJUb2tB.mjs} +113 -6
- package/dist/toolchain-BIupIro1.mjs +2 -0
- package/dist/{toolchain-Bnyd_K3A.mjs → toolchain-DqyDXs30.mjs} +225 -315
- package/dist/{trigger-CJMhwXe8.mjs → trigger-C0MWBi8O.mjs} +3 -3
- package/dist/trigger-SHLOWJ4O.mjs +2 -0
- package/dist/{warm-progress-CjB-MP6H.mjs → warm-progress-C3ASD6z5.mjs} +134 -37
- package/dist/{status-Dcr0Qexd.mjs → watchman-CQl51YZu.mjs} +75 -7
- package/dist/web-B3eldHFH.mjs +123 -0
- package/dist/web-BLXpOe6b.mjs +2 -0
- package/dist/web-run.mjs +10 -10
- package/dist/{workspace-state-C59dqG-Z.mjs → workspace-state-CSIFuuDa.mjs} +33 -33
- package/dist/{workspaces-xXItD8Dj.mjs → workspaces-D2xSHBDY.mjs} +1297 -1211
- package/dist/{worktree-BSYlofkq.mjs → worktree-BxDqaTev.mjs} +102 -63
- package/dist/worktree-q7_OZNY0.mjs +2 -0
- package/package.json +8 -6
- package/shim/bundle-response.cjs +2 -1
- package/shim/bundle-response.d.cts +1 -1
- package/shim/expo-metro-config.cjs +1 -1
- package/shim/native-android.gradle +101 -0
- package/dist/android-BKA16DTf.mjs +0 -4
- package/dist/android-BvwuMUJ9.mjs +0 -3572
- package/dist/guide-status-Bhf9SJXc.mjs +0 -125
- package/dist/ios-CPdkoKaa.mjs +0 -3953
- package/dist/ios-DNpdygiv.mjs +0 -7
- package/dist/macos-Ck7OBMNY.mjs +0 -2
- package/dist/metro-store-BAHFcJm6.mjs +0 -106
- package/dist/plan-placement-CtxkCO54.mjs +0 -1859
- package/dist/project-C3ChRLZy.mjs +0 -212
- package/dist/projects-DD7dBTCj.mjs +0 -92
- package/dist/server-expo-Db9HBUoa.mjs +0 -2
- package/dist/settings-CQyMElEP.mjs +0 -701
- package/dist/start-NTc1Ifqs.mjs +0 -2
- package/dist/stop-BGhKMOuH.mjs +0 -48
- package/dist/trigger-DWAq9hkw.mjs +0 -2
- package/dist/web-B6_bFDn7.mjs +0 -2
- package/dist/worktree-BJfnU6r5.mjs +0 -2
- package/dist/worktree-CsKYexJM.mjs +0 -753
|
@@ -1,57 +1,31 @@
|
|
|
1
1
|
import { n as ownership_claim_exports, t as process_identity_exports } from "./process-identity-Biv3arCO.mjs";
|
|
2
|
-
import { t as getExecutor } from "./exec-
|
|
2
|
+
import { t as getExecutor } from "./exec-B81Qodjn.mjs";
|
|
3
|
+
import { D as withConfigLock$1, H as projectDeviceSlots, M as isOnMountedVolume, N as listMountedVolumes, P as measuredDirectorySize, _ as loadConfig$1, d as getConfigDir, g as isPathPrefix, j as formatBytes, k as acquireAvdClaim, p as getProject, z as deviceSlotPlatforms } from "./config-D0LhTY3S.mjs";
|
|
4
|
+
import { Et as repoRoot, st as resolveTailscaleBinary } from "./settings-Bbztm4oV.mjs";
|
|
5
|
+
import { h as listBuildLocks } from "./device-lease-DHOBXGgS.mjs";
|
|
6
|
+
import { T as workspaceName, r as readWorkspaceState$1, s as workspaceLastUsed, x as workspaceDir, y as workspaceAgentDeviceDir } from "./workspace-state-CSIFuuDa.mjs";
|
|
3
7
|
import { t as createLineReader } from "./process-output-Fpl8PYi8.mjs";
|
|
4
|
-
import { a as formatElapsed, l as phaseLine } from "./command-output-
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { S as workspaceName, g as workspaceAgentDeviceDir, r as readWorkspaceState$1, s as workspaceLastUsed, v as workspaceDir } from "./workspace-state-C59dqG-Z.mjs";
|
|
9
|
-
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, u as webFacts } from "./state-BG-aAk4k.mjs";
|
|
10
|
-
import { t as agentDeviceDaemon, u as readAgentDeviceRecords } from "./activity-BxnkDZDi.mjs";
|
|
11
|
-
import { h as listBuildLocks } from "./device-lease-Co-EwifR.mjs";
|
|
8
|
+
import { a as formatElapsed, l as phaseLine } from "./command-output-BjPN99ZB.mjs";
|
|
9
|
+
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, ct as eraseParkedAfter, et as waitForAndroidEmulatorShutdown, f as assertOwnedAvdStopped, ft as readParked, h as avdPathExists, nt as wipeAvdUserData, pt as removeParkedAfter, rt as isStimOwnedAvd, ut as parkSim, x as deleteAvd } from "./android-BsYNR81W.mjs";
|
|
10
|
+
import { n as readCreatedDevices$1, r as recordCreatedDevice, t as forgetCreatedDevice } from "./created-devices-HU_DAb_Q.mjs";
|
|
11
|
+
import { d as signalProcessTree, s as pidExists } from "./metro-D_j9sN-B.mjs";
|
|
12
12
|
import { n as declareSpawnsOn, o as stopDeclaringSpawnsOn } from "./spawn-claims-Kqiifsk2.mjs";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { a as
|
|
18
|
-
import { i as macosRuntimeClaim } from "./state-xnqpx3AD.mjs";
|
|
13
|
+
import { a as withWorkspaceProcessLock, o as workspaceProcessLockError, r as releaseBrowserPort, s as workspaceProcessLockPath } from "./named-ports-C2lvoPsb.mjs";
|
|
14
|
+
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, u as webFacts } from "./state-DI84A-uu.mjs";
|
|
15
|
+
import { t as agentDeviceDaemon, u as readAgentDeviceRecords } from "./activity-BrGh9EAI.mjs";
|
|
16
|
+
import { E as resolveOwnedIosSim, O as shutdownIosSim, S as renameIosSim, c as eraseIosSim, o as deleteIosSim, p as listIosDeviceTypes, s as deleteParkedIosSim, v as parkedSimName, y as parseRuntimeVersion } from "./ios-CXLIUxXJ.mjs";
|
|
17
|
+
import { a as macosRuntimeClaim, n as macosDir } from "./state-Boo0IMJq.mjs";
|
|
19
18
|
import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync } from "node:fs";
|
|
20
19
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
21
20
|
import { basename as basename$1, isAbsolute as isAbsolute$1, join as join$1, relative as relative$1, resolve as resolve$1 } from "path";
|
|
22
|
-
import { buildSlotsDir, hostedNativeRecords, isJsonObject, macosAppState, parseHostedNativePlacement, readJsonFile, readMacosRecord, tryAcquireBuildSlotClaim, unreadableHostedNative } from "@stim-cli/core/state";
|
|
21
|
+
import { buildSlotsDir, hostedNativeRecords, isJsonObject, macosAppState, parseHostedNativePlacement, readJsonFile, readMacosRecord, requireAutomaticMachine, tryAcquireBuildSlotClaim, unreadableHostedNative } from "@stim-cli/core/state";
|
|
22
|
+
import { existsSync as existsSync$1, lstatSync as lstatSync$1, readdirSync as readdirSync$1, realpathSync as realpathSync$1, rmSync as rmSync$1, statSync as statSync$1 } from "fs";
|
|
23
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
23
24
|
import { homedir } from "node:os";
|
|
24
25
|
import chalk from "chalk";
|
|
25
26
|
import { isDeepStrictEqual } from "node:util";
|
|
26
|
-
import { existsSync as existsSync$1, lstatSync as lstatSync$1, readdirSync as readdirSync$1, realpathSync as realpathSync$1, rmSync as rmSync$1, statSync as statSync$1 } from "fs";
|
|
27
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
28
27
|
import { resolve4 } from "node:dns/promises";
|
|
29
28
|
import { request } from "node:https";
|
|
30
|
-
//#region src/commands/gc/paths.ts
|
|
31
|
-
function canonicalPath(p) {
|
|
32
|
-
try {
|
|
33
|
-
return realpathSync$1(p);
|
|
34
|
-
} catch {
|
|
35
|
-
return resolve$1(p);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
//#endregion
|
|
39
|
-
//#region src/commands/gc/results.ts
|
|
40
|
-
const results = [];
|
|
41
|
-
function recordGcResult(kind, status, label, { id = null, bytes = null, detail = null } = {}) {
|
|
42
|
-
results.push({
|
|
43
|
-
kind,
|
|
44
|
-
status,
|
|
45
|
-
label,
|
|
46
|
-
id,
|
|
47
|
-
bytes,
|
|
48
|
-
detail
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
function takeGcResults() {
|
|
52
|
-
return results.splice(0);
|
|
53
|
-
}
|
|
54
|
-
//#endregion
|
|
55
29
|
//#region src/engine/build-slots.ts
|
|
56
30
|
const SLOT_PREFIX = "slot-";
|
|
57
31
|
const SLOT_POLL_MS = 1e3;
|
|
@@ -101,7 +75,8 @@ function tryAcquireBuildSlot({ max, root = null, logFile = null }) {
|
|
|
101
75
|
function slotWaitingLine({ max, elapsedMs }) {
|
|
102
76
|
return `${"build".padEnd(11)} waiting for a build slot (all ${max} in use, ${formatElapsed(elapsedMs)} elapsed) -- stim guide lifecycle concurrency`;
|
|
103
77
|
}
|
|
104
|
-
async function acquireBuildSlot({ max, root = null, logFile = null, now = Date.now, out = () => {}, waitingFor = () => {}, sleep = sleepAsync$1, intervalMs = SLOT_POLL_MS, progressMs = SLOT_PROGRESS_MS, ceilingMs = SLOT_CEILING_MS }) {
|
|
78
|
+
async function acquireBuildSlot({ max, root = null, logFile = null, now = Date.now, out = () => {}, waitingFor = () => {}, sleep = sleepAsync$1, intervalMs = SLOT_POLL_MS, progressMs = SLOT_PROGRESS_MS, ceilingMs = SLOT_CEILING_MS, automatic = false }) {
|
|
79
|
+
if (automatic) requireAutomaticMachine("build", "local");
|
|
105
80
|
if (!max || max <= 0) return {
|
|
106
81
|
acquired: true,
|
|
107
82
|
unlimited: true
|
|
@@ -111,6 +86,7 @@ async function acquireBuildSlot({ max, root = null, logFile = null, now = Date.n
|
|
|
111
86
|
let waited = false;
|
|
112
87
|
try {
|
|
113
88
|
for (;;) {
|
|
89
|
+
if (automatic) requireAutomaticMachine("build", "local");
|
|
114
90
|
const got = tryAcquireBuildSlot({
|
|
115
91
|
max,
|
|
116
92
|
root,
|
|
@@ -187,1285 +163,1389 @@ function listBuildSlots() {
|
|
|
187
163
|
return slots;
|
|
188
164
|
}
|
|
189
165
|
//#endregion
|
|
190
|
-
//#region src/
|
|
191
|
-
function
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
166
|
+
//#region src/supervisor/ownership.ts
|
|
167
|
+
function resolveSupervisorTarget({ state, record, reservedPort, isAlive = pidExists, inspectIdentity = process_identity_exports.inspectProcessIdentity } = {}) {
|
|
168
|
+
const statePid = numberOrNull(state?.pid);
|
|
169
|
+
const recordPid = numberOrNull(record?.pid);
|
|
170
|
+
const pid = statePid ?? recordPid;
|
|
171
|
+
if (!pid) return { status: "none" };
|
|
172
|
+
if (statePid && recordPid && statePid !== recordPid) return {
|
|
173
|
+
status: "unverified",
|
|
174
|
+
pid,
|
|
175
|
+
reason: `workspace state.json records supervisor pid ${statePid} but the registry records pid ${recordPid}`
|
|
176
|
+
};
|
|
177
|
+
if (!isAlive(pid)) return {
|
|
178
|
+
status: "stale",
|
|
179
|
+
pid
|
|
180
|
+
};
|
|
181
|
+
const port = numberOrNull(state?.port) ?? numberOrNull(record?.port);
|
|
182
|
+
if (reservedPort !== null && reservedPort !== void 0 && port !== null && port !== reservedPort) return {
|
|
183
|
+
status: "unverified",
|
|
184
|
+
pid,
|
|
185
|
+
reason: `supervisor pid ${pid} records port ${port}, but this project reserved port ${reservedPort}`
|
|
186
|
+
};
|
|
187
|
+
if (reservedPort !== null && reservedPort !== void 0 && port === null) return {
|
|
188
|
+
status: "unverified",
|
|
189
|
+
pid,
|
|
190
|
+
reason: `supervisor pid ${pid} has no recorded port, so it cannot be matched against reserved port ${reservedPort}`
|
|
191
|
+
};
|
|
192
|
+
const identityRecord = statePid ? state : record;
|
|
193
|
+
if (statePid && recordPid && state?.processToken !== record?.processToken) return {
|
|
194
|
+
status: "unverified",
|
|
195
|
+
pid,
|
|
196
|
+
reason: "workspace state and registry process identities disagree"
|
|
197
|
+
};
|
|
198
|
+
const identity = inspectIdentity(identityRecord);
|
|
199
|
+
if (identity === "gone" || identity === "different") return {
|
|
200
|
+
status: "stale",
|
|
201
|
+
pid
|
|
202
|
+
};
|
|
203
|
+
if (identity !== "same") return {
|
|
204
|
+
status: "unverified",
|
|
205
|
+
pid,
|
|
206
|
+
reason: "the recorded process identity could not be verified"
|
|
207
|
+
};
|
|
208
|
+
return {
|
|
209
|
+
status: "ours",
|
|
210
|
+
processToken: identityRecord.processToken,
|
|
211
|
+
pid,
|
|
212
|
+
port,
|
|
213
|
+
mode: state?.mode ?? record?.mode ?? null,
|
|
214
|
+
startedAt: state?.startedAt ?? record?.startedAt ?? null
|
|
215
|
+
};
|
|
207
216
|
}
|
|
208
|
-
function
|
|
209
|
-
|
|
210
|
-
if (device.platform === "android" && session.device !== device.avdName.replaceAll("_", " ")) return false;
|
|
211
|
-
if (!owner) return true;
|
|
212
|
-
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));
|
|
217
|
+
function numberOrNull(v) {
|
|
218
|
+
return typeof v === "number" && Number.isSafeInteger(v) && v > 0 ? v : null;
|
|
213
219
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region src/engine/public-http-probe.ts
|
|
222
|
+
const DNS_TIMEOUT_MS = 5e3;
|
|
223
|
+
async function resolveAddresses(hostname, signal) {
|
|
224
|
+
if (signal.aborted) throw signal.reason;
|
|
225
|
+
let timer;
|
|
226
|
+
let onAbort;
|
|
227
|
+
const unavailable = new Promise((_resolve, reject) => {
|
|
228
|
+
onAbort = () => reject(signal.reason);
|
|
229
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
230
|
+
if (signal.aborted) onAbort();
|
|
231
|
+
else timer = setTimeout(() => reject(/* @__PURE__ */ new Error("DNS lookup timed out")), DNS_TIMEOUT_MS);
|
|
232
|
+
});
|
|
233
|
+
try {
|
|
234
|
+
return await Promise.race([resolve4(hostname), unavailable]);
|
|
235
|
+
} finally {
|
|
236
|
+
clearTimeout(timer);
|
|
237
|
+
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
238
|
+
}
|
|
217
239
|
}
|
|
218
|
-
function
|
|
240
|
+
async function probePublicHttp(url, signal) {
|
|
219
241
|
try {
|
|
220
|
-
return
|
|
242
|
+
return (await fetch(url, {
|
|
243
|
+
signal,
|
|
244
|
+
redirect: "follow"
|
|
245
|
+
})).status;
|
|
246
|
+
} catch (error) {
|
|
247
|
+
const cause = error?.cause;
|
|
248
|
+
if (cause?.code !== "ENOTFOUND" && cause?.code !== "EAI_AGAIN") return null;
|
|
249
|
+
}
|
|
250
|
+
const hostname = new URL(url).hostname;
|
|
251
|
+
if (!hostname.endsWith(".trycloudflare.com")) return null;
|
|
252
|
+
let addresses;
|
|
253
|
+
try {
|
|
254
|
+
addresses = await resolveAddresses(hostname, signal);
|
|
221
255
|
} catch {
|
|
222
256
|
return null;
|
|
223
257
|
}
|
|
258
|
+
const firstAddress = addresses[0];
|
|
259
|
+
if (!firstAddress) return null;
|
|
260
|
+
return new Promise((resolve) => {
|
|
261
|
+
const request$1 = request(url, {
|
|
262
|
+
signal,
|
|
263
|
+
lookup: (_hostname, options, callback) => {
|
|
264
|
+
if (options.all) callback(null, addresses.map((address) => ({
|
|
265
|
+
address,
|
|
266
|
+
family: 4
|
|
267
|
+
})));
|
|
268
|
+
else callback(null, firstAddress, 4);
|
|
269
|
+
}
|
|
270
|
+
}, (response) => {
|
|
271
|
+
response.resume();
|
|
272
|
+
resolve(response.statusCode ?? null);
|
|
273
|
+
});
|
|
274
|
+
request$1.on("error", () => resolve(null));
|
|
275
|
+
request$1.end();
|
|
276
|
+
});
|
|
224
277
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
278
|
+
//#endregion
|
|
279
|
+
//#region src/engine/tunnel.ts
|
|
280
|
+
function tunnelArgv(provider, port, ngrokUrl, logFile, tailscaleBinary = "tailscale") {
|
|
281
|
+
if (provider === "tailscale") return {
|
|
282
|
+
bin: tailscaleBinary,
|
|
283
|
+
args: [
|
|
284
|
+
"serve",
|
|
285
|
+
`--https=${port}`,
|
|
286
|
+
`http://127.0.0.1:${port}`
|
|
287
|
+
]
|
|
288
|
+
};
|
|
289
|
+
if (provider === "cloudflared") return {
|
|
290
|
+
bin: "cloudflared",
|
|
291
|
+
args: [
|
|
292
|
+
"tunnel",
|
|
293
|
+
"--url",
|
|
294
|
+
`http://127.0.0.1:${port}`,
|
|
295
|
+
...logFile ? ["--logfile", logFile] : []
|
|
296
|
+
]
|
|
297
|
+
};
|
|
298
|
+
return {
|
|
299
|
+
bin: "ngrok",
|
|
300
|
+
args: [
|
|
301
|
+
"http",
|
|
302
|
+
String(port),
|
|
303
|
+
`--log=${logFile || "stdout"}`,
|
|
304
|
+
"--log-format=json",
|
|
305
|
+
...ngrokUrl ? ["--url", ngrokUrl] : []
|
|
306
|
+
]
|
|
307
|
+
};
|
|
231
308
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
309
|
+
const CLOUDFLARED_URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/i;
|
|
310
|
+
function parseCloudflaredLine(line) {
|
|
311
|
+
const match = line.match(CLOUDFLARED_URL_RE);
|
|
312
|
+
return match ? match[0] : null;
|
|
313
|
+
}
|
|
314
|
+
function parseNgrokLine(line) {
|
|
315
|
+
let data;
|
|
235
316
|
try {
|
|
236
|
-
|
|
317
|
+
data = JSON.parse(line);
|
|
237
318
|
} catch {
|
|
238
|
-
|
|
319
|
+
return null;
|
|
239
320
|
}
|
|
240
|
-
if (typeof
|
|
241
|
-
const
|
|
242
|
-
return
|
|
321
|
+
if (typeof data !== "object" || data === null) return null;
|
|
322
|
+
const url = data.url;
|
|
323
|
+
return typeof url === "string" && /^https?:\/\//i.test(url) ? url : null;
|
|
243
324
|
}
|
|
244
|
-
function
|
|
245
|
-
|
|
325
|
+
function parseTailscaleLine(line) {
|
|
326
|
+
return /^https:\/\/[^\s/]+\.ts\.net(:\d+)?\/?$/.test(line) ? line : null;
|
|
246
327
|
}
|
|
247
|
-
function
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (!exec.findExecutable("agent-device")) return;
|
|
251
|
-
const root = workspace === void 0 ? void 0 : canonical(workspace);
|
|
252
|
-
if (root === null) return;
|
|
253
|
-
const owner = root === void 0 ? void 0 : {
|
|
254
|
-
workspace: root,
|
|
255
|
-
repoRoot: repoRoot && (canonical(repoRoot) ?? void 0),
|
|
256
|
-
claims: deviceClaims(device)
|
|
257
|
-
};
|
|
258
|
-
const stateDir = root === void 0 ? void 0 : workspaceAgentDeviceDir(root);
|
|
259
|
-
const stateDirs = [void 0, ...stateDir && !["none", "dead"].includes(agentDeviceDaemon(stateDir)) ? [stateDir] : []];
|
|
260
|
-
for (const dir of stateDirs) {
|
|
261
|
-
const deadline = Date.now() + 15e3;
|
|
262
|
-
const run = (args) => {
|
|
263
|
-
const remaining = deadline - Date.now();
|
|
264
|
-
if (remaining <= 0) throw new Error("agent-device cleanup timed out");
|
|
265
|
-
try {
|
|
266
|
-
return exec.runFile("agent-device", [
|
|
267
|
-
...args,
|
|
268
|
-
...dir ? ["--state-dir", dir] : [],
|
|
269
|
-
"--json",
|
|
270
|
-
"--daemon-transport",
|
|
271
|
-
"socket"
|
|
272
|
-
], {
|
|
273
|
-
timeoutMs: Math.min(5e3, remaining),
|
|
274
|
-
killSignal: "SIGKILL"
|
|
275
|
-
});
|
|
276
|
-
} catch (error) {
|
|
277
|
-
throw explainFailure(error);
|
|
278
|
-
}
|
|
279
|
-
};
|
|
280
|
-
const list = () => parseAgentDeviceSessions(run([
|
|
281
|
-
"session",
|
|
282
|
-
"list",
|
|
283
|
-
"--session",
|
|
284
|
-
"stim-teardown-inventory"
|
|
285
|
-
]));
|
|
286
|
-
try {
|
|
287
|
-
const sessions = list().filter((session) => isOwnDeviceSession(session, device, owner));
|
|
288
|
-
for (const session of sessions) try {
|
|
289
|
-
if (!list().some((current) => current.name === session.name && current.platform === session.platform && current.id === session.id && current.createdAt === session.createdAt)) continue;
|
|
290
|
-
if (!stillOwned(session.id)) continue;
|
|
291
|
-
if (JSON.parse(run([
|
|
292
|
-
"close",
|
|
293
|
-
"--session",
|
|
294
|
-
session.name,
|
|
295
|
-
"--session-lock",
|
|
296
|
-
"reject",
|
|
297
|
-
"--platform",
|
|
298
|
-
device.platform,
|
|
299
|
-
device.platform === "ios" ? "--udid" : "--serial",
|
|
300
|
-
session.id
|
|
301
|
-
]))?.success !== true) throw new Error("agent-device did not confirm session close");
|
|
302
|
-
report(`closed agent-device session ${session.name} on ${session.id}`);
|
|
303
|
-
} catch (error) {
|
|
304
|
-
report(`could not close agent-device session ${session.name} on ${session.id}: ${error.message}`);
|
|
305
|
-
}
|
|
306
|
-
} catch (error) {
|
|
307
|
-
report(`could not list agent-device sessions for ${device.platform === "ios" ? device.id : device.id ?? device.avdName}: ${error.message}`);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
} catch (error) {
|
|
311
|
-
report(`could not list agent-device sessions for ${device.platform === "ios" ? device.id : device.id ?? device.avdName}: ${error.message}`);
|
|
312
|
-
}
|
|
328
|
+
function parserFor(provider) {
|
|
329
|
+
if (provider === "tailscale") return parseTailscaleLine;
|
|
330
|
+
return provider === "cloudflared" ? parseCloudflaredLine : parseNgrokLine;
|
|
313
331
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
if (result.status !== "torn-down" && result.status !== "missing") throw new Error(result.reason);
|
|
324
|
-
}) ? {
|
|
325
|
-
status: "torn-down",
|
|
326
|
-
label: avdName
|
|
327
|
-
} : {
|
|
328
|
-
status: "skipped",
|
|
329
|
-
kind: "not-parked",
|
|
330
|
-
reason: "emulator is no longer parked"
|
|
331
|
-
};
|
|
332
|
-
} catch (error) {
|
|
333
|
-
return {
|
|
334
|
-
status: "failed",
|
|
335
|
-
reason: String(error?.message || error)
|
|
336
|
-
};
|
|
337
|
-
}
|
|
332
|
+
const URL_TIMEOUT_MS = 15e3;
|
|
333
|
+
const REACHABLE_TIMEOUT_MS = 24e4;
|
|
334
|
+
const REACHABLE_POLL_MS = 2e3;
|
|
335
|
+
const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
336
|
+
async function withManagedTunnelLock(root, fn, options = {}) {
|
|
337
|
+
return withWorkspaceProcessLock(managedTunnelLockRoot(root), "metro-tunnel", fn, {
|
|
338
|
+
...options,
|
|
339
|
+
rejectOwnerPurposes: ["workspace removal"]
|
|
340
|
+
});
|
|
338
341
|
}
|
|
339
|
-
function
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
342
|
+
async function withManagedTunnelRemovalLock(root, fn, options = {}) {
|
|
343
|
+
return withWorkspaceProcessLock(managedTunnelLockRoot(root), "metro-tunnel", fn, {
|
|
344
|
+
...options,
|
|
345
|
+
ownerPurpose: "workspace removal"
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
async function withManagedRemoteWorktreeLock(worktreeRoot, fn, options = {}) {
|
|
349
|
+
return withWorkspaceProcessLock(managedRemoteWorktreeLockRoot(worktreeRoot), "managed-remote", fn, {
|
|
350
|
+
...options,
|
|
351
|
+
external: true,
|
|
352
|
+
rejectOwnerPurposes: ["worktree removal"]
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
async function withManagedRemoteWorktreeRemovalLock(worktreeRoot, fn, options = {}) {
|
|
356
|
+
return withWorkspaceProcessLock(managedRemoteWorktreeLockRoot(worktreeRoot), "managed-remote", fn, {
|
|
357
|
+
...options,
|
|
358
|
+
external: true,
|
|
359
|
+
ownerPurpose: "worktree removal"
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
function managedLockHolders(root) {
|
|
363
|
+
const locks = [{
|
|
364
|
+
label: "managed tunnel",
|
|
365
|
+
path: workspaceProcessLockPath(managedTunnelLockRoot(root), "metro-tunnel", false)
|
|
366
|
+
}, {
|
|
367
|
+
label: "managed remote",
|
|
368
|
+
path: workspaceProcessLockPath(managedRemoteWorktreeLockRoot(root), "managed-remote", true)
|
|
369
|
+
}];
|
|
370
|
+
const reasons = [];
|
|
371
|
+
for (const { label, path } of locks) {
|
|
372
|
+
const claims = (0, ownership_claim_exports.readClaimSet)(path);
|
|
373
|
+
const holder = claims.live[0];
|
|
374
|
+
if (holder) {
|
|
375
|
+
const purpose = typeof holder.details.purpose === "string" ? ` for ${holder.details.purpose}` : "";
|
|
376
|
+
reasons.push(`the ${label} lock is held${purpose}`);
|
|
377
|
+
} else if (claims.unresolved[0]) reasons.push(`the ${label} lock cannot be resolved: ${claims.unresolved[0].reason}`);
|
|
362
378
|
}
|
|
379
|
+
return reasons;
|
|
363
380
|
}
|
|
364
|
-
function
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
381
|
+
function managedTunnelLockRoot(root) {
|
|
382
|
+
const key = createHash("sha256").update(resolve(root)).digest("hex");
|
|
383
|
+
return join(getConfigDir(), "process-locks", key);
|
|
384
|
+
}
|
|
385
|
+
function managedRemoteWorktreeLockRoot(worktreeRoot) {
|
|
386
|
+
const key = createHash("sha256").update(resolve(worktreeRoot)).digest("hex");
|
|
387
|
+
return join(getConfigDir(), "process-locks", "worktrees", key);
|
|
388
|
+
}
|
|
389
|
+
async function defaultProbeReachable(url, signal) {
|
|
390
|
+
const status = await probePublicHttp(url, signal);
|
|
391
|
+
return status !== null && status > 0;
|
|
392
|
+
}
|
|
393
|
+
function waitForUrl(child, parseLine, timeoutMs) {
|
|
394
|
+
return new Promise((resolve) => {
|
|
395
|
+
let settled = false;
|
|
396
|
+
let lastLine = null;
|
|
397
|
+
let timer;
|
|
398
|
+
let onOut;
|
|
399
|
+
let onErr;
|
|
400
|
+
let onError;
|
|
401
|
+
let onExit;
|
|
402
|
+
const finish = (url, exited = false) => {
|
|
403
|
+
if (settled) return;
|
|
404
|
+
settled = true;
|
|
405
|
+
clearTimeout(timer);
|
|
406
|
+
child.stdout?.removeListener("data", onOut);
|
|
407
|
+
child.stderr?.removeListener("data", onErr);
|
|
408
|
+
child.removeListener("error", onError);
|
|
409
|
+
child.removeListener("exit", onExit);
|
|
410
|
+
resolve({
|
|
411
|
+
url,
|
|
412
|
+
exited,
|
|
413
|
+
lastLine
|
|
414
|
+
});
|
|
383
415
|
};
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
416
|
+
const onLine = (line) => {
|
|
417
|
+
if (settled) return;
|
|
418
|
+
if (line.trim()) lastLine = line.trim();
|
|
419
|
+
const url = parseLine(line);
|
|
420
|
+
if (url) finish(url);
|
|
387
421
|
};
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
422
|
+
const outReader = createLineReader(onLine);
|
|
423
|
+
const errReader = createLineReader(onLine);
|
|
424
|
+
onOut = (chunk) => outReader.push(chunk);
|
|
425
|
+
onErr = (chunk) => errReader.push(chunk);
|
|
426
|
+
onError = () => finish(null);
|
|
427
|
+
onExit = () => {
|
|
428
|
+
outReader.flush();
|
|
429
|
+
errReader.flush();
|
|
430
|
+
finish(null, true);
|
|
392
431
|
};
|
|
393
|
-
|
|
432
|
+
child.stdout?.setEncoding?.("utf-8");
|
|
433
|
+
child.stderr?.setEncoding?.("utf-8");
|
|
434
|
+
child.stdout?.on("data", onOut);
|
|
435
|
+
child.stderr?.on("data", onErr);
|
|
436
|
+
child.on("error", onError);
|
|
437
|
+
child.on("exit", onExit);
|
|
438
|
+
timer = setTimeout(() => finish(null), timeoutMs);
|
|
439
|
+
});
|
|
394
440
|
}
|
|
395
|
-
function
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
441
|
+
function waitForFileUrl(child, logFile, parseLine, timeoutMs) {
|
|
442
|
+
return new Promise((resolve) => {
|
|
443
|
+
let settled = false;
|
|
444
|
+
const finish = (url, exited = false) => {
|
|
445
|
+
if (settled) return;
|
|
446
|
+
settled = true;
|
|
447
|
+
clearInterval(interval);
|
|
448
|
+
clearTimeout(timer);
|
|
449
|
+
child.removeListener("error", onError);
|
|
450
|
+
child.removeListener("exit", onExit);
|
|
451
|
+
resolve({
|
|
452
|
+
url,
|
|
453
|
+
exited
|
|
403
454
|
});
|
|
404
|
-
if (result.status !== "torn-down") throw new Error(result.reason ?? `AVD ${avdName} is ${result.status}`);
|
|
405
|
-
}) ? {
|
|
406
|
-
status: "torn-down",
|
|
407
|
-
label: avdName
|
|
408
|
-
} : {
|
|
409
|
-
status: "skipped",
|
|
410
|
-
kind: "not-parked",
|
|
411
|
-
reason: "emulator is no longer parked"
|
|
412
455
|
};
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
456
|
+
const probe = () => {
|
|
457
|
+
try {
|
|
458
|
+
for (const line of readFileSync(logFile, "utf-8").split(/\r?\n/)) {
|
|
459
|
+
const url = parseLine(line);
|
|
460
|
+
if (url) return finish(url);
|
|
461
|
+
}
|
|
462
|
+
} catch {}
|
|
417
463
|
};
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
return root && realpathSync(root) !== realpathSync(workspace) ? root : void 0;
|
|
427
|
-
} catch {
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
464
|
+
const onError = () => finish(null);
|
|
465
|
+
const onExit = () => finish(null, true);
|
|
466
|
+
child.on("error", onError);
|
|
467
|
+
child.on("exit", onExit);
|
|
468
|
+
const interval = setInterval(probe, 25);
|
|
469
|
+
const timer = setTimeout(() => finish(null), timeoutMs);
|
|
470
|
+
probe();
|
|
471
|
+
});
|
|
430
472
|
}
|
|
431
|
-
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
while (resolved.sim && resolved.sim.state !== "Shutdown" && now() < deadline) {
|
|
438
|
-
sleep(IOS_SHUTDOWN_POLL_MS);
|
|
439
|
-
resolved = resolveOwnedIosSim(udid);
|
|
440
|
-
}
|
|
441
|
-
if (!resolved.sim || resolved.sim.state === "Shutdown" || attempt === IOS_SHUTDOWN_ATTEMPTS) return resolved;
|
|
442
|
-
shutdownIosSim(udid);
|
|
443
|
-
resolved = resolveOwnedIosSim(udid);
|
|
444
|
-
}
|
|
473
|
+
function createTunnelLogFile(provider) {
|
|
474
|
+
const dir = join(getConfigDir(), "tunnel-logs");
|
|
475
|
+
mkdirSync(dir, { recursive: true });
|
|
476
|
+
const path = join(dir, `${provider}-${randomUUID()}.log`);
|
|
477
|
+
closeSync(openSync(path, "wx", 384));
|
|
478
|
+
return path;
|
|
445
479
|
}
|
|
446
|
-
function
|
|
447
|
-
|
|
480
|
+
function removeTunnelLogFile(path) {
|
|
481
|
+
if (!path) return;
|
|
482
|
+
try {
|
|
483
|
+
unlinkSync(path);
|
|
484
|
+
} catch {}
|
|
448
485
|
}
|
|
449
|
-
function
|
|
450
|
-
if (
|
|
451
|
-
|
|
452
|
-
const
|
|
453
|
-
if (
|
|
454
|
-
|
|
455
|
-
const runtime = parseRuntimeVersion(sim.runtime);
|
|
456
|
-
const name = parkedSimName(sim.udid, {
|
|
457
|
-
model,
|
|
458
|
-
runtime
|
|
459
|
-
});
|
|
460
|
-
renameIosSim(sim.udid, name);
|
|
461
|
-
const record = {
|
|
462
|
-
udid: sim.udid,
|
|
463
|
-
name,
|
|
464
|
-
deviceTypeIdentifier: sim.deviceTypeIdentifier,
|
|
465
|
-
runtimeIdentifier: sim.runtime,
|
|
466
|
-
parkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
467
|
-
simslimManaged: Boolean(park.simslimManaged),
|
|
468
|
-
...park.appId ? { bundleId: park.appId } : {},
|
|
469
|
-
...park.cacheKey ? { cacheKey: park.cacheKey } : {}
|
|
470
|
-
};
|
|
471
|
-
return {
|
|
472
|
-
record,
|
|
473
|
-
evicted: parkSim({
|
|
474
|
-
platform: "ios",
|
|
475
|
-
projectPath: park.projectPath,
|
|
476
|
-
slot: park.slot,
|
|
477
|
-
record,
|
|
478
|
-
max: park.max
|
|
479
|
-
})
|
|
480
|
-
};
|
|
486
|
+
function removeRecordedTunnelLogFile(path) {
|
|
487
|
+
if (!path) return;
|
|
488
|
+
const dir = resolve(join(getConfigDir(), "tunnel-logs"));
|
|
489
|
+
const resolved = resolve(path);
|
|
490
|
+
if (!resolved.startsWith(`${dir}${sep}`) || resolve(join(dir, basename(resolved))) !== resolved) return;
|
|
491
|
+
removeTunnelLogFile(resolved);
|
|
481
492
|
}
|
|
482
|
-
function
|
|
483
|
-
|
|
493
|
+
async function waitUntilReachable({ url, now, sleep, probe, timeoutMs }) {
|
|
494
|
+
const deadline = now() + timeoutMs;
|
|
495
|
+
for (;;) {
|
|
496
|
+
if (await probe(url, new AbortController().signal).catch(() => false)) return true;
|
|
497
|
+
if (now() >= deadline) return false;
|
|
498
|
+
await sleep(REACHABLE_POLL_MS);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
const CLEANUP_TIMEOUT_MS = 1e3;
|
|
502
|
+
const CLEANUP_POLL_MS = 25;
|
|
503
|
+
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 }) {
|
|
504
|
+
const spawn = spawnFn || ((cmd, args, opts) => getExecutor().spawn(cmd, args, opts));
|
|
505
|
+
const outputFile = provider === "tailscale" ? null : logFile || (spawnFn ? null : createTunnelLogFile(provider));
|
|
506
|
+
let child;
|
|
484
507
|
try {
|
|
485
|
-
const
|
|
486
|
-
if (
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
};
|
|
503
|
-
shutdownIosSim(udid);
|
|
504
|
-
const sim = resolved.sim;
|
|
505
|
-
if (del && park && park.max > 0) try {
|
|
506
|
-
const { record, evicted } = parkOwnedIosSim(settleIosSimShutdown(udid, shutdownClock), udid, park);
|
|
507
|
-
const removed = [];
|
|
508
|
-
const failures = [];
|
|
509
|
-
for (const entry of evicted) {
|
|
510
|
-
const result = teardownParkedIosSim(entry.udid, { label: entry.name });
|
|
511
|
-
if (result.status === "torn-down") removed.push({
|
|
512
|
-
udid: entry.udid,
|
|
513
|
-
name: entry.name
|
|
514
|
-
});
|
|
515
|
-
else if (result.status === "failed") failures.push(`could not delete evicted ${entry.name} (${entry.udid}): ${result.reason}`);
|
|
516
|
-
}
|
|
517
|
-
return {
|
|
518
|
-
status: "torn-down",
|
|
519
|
-
label: label ?? sim.name ?? udid,
|
|
520
|
-
parked: {
|
|
521
|
-
udid: record.udid,
|
|
522
|
-
name: record.name
|
|
523
|
-
},
|
|
524
|
-
evicted: removed,
|
|
525
|
-
...failures.length ? { evictionFailures: failures } : {}
|
|
526
|
-
};
|
|
527
|
-
} catch (e) {
|
|
528
|
-
parkFallback = String(e?.message || e);
|
|
529
|
-
}
|
|
530
|
-
if (del) {
|
|
531
|
-
deleteIosSim(udid);
|
|
532
|
-
return {
|
|
533
|
-
status: "torn-down",
|
|
534
|
-
label: label ?? sim.name ?? udid,
|
|
535
|
-
...parkFallback ? { parkFallback } : {}
|
|
536
|
-
};
|
|
537
|
-
}
|
|
538
|
-
const settled = settleIosSimShutdown(udid, shutdownClock);
|
|
539
|
-
if (settled.sim && settled.sim.state !== "Shutdown") return {
|
|
540
|
-
status: "failed",
|
|
541
|
-
label: label ?? sim.name ?? udid,
|
|
542
|
-
reason: iosShutdownFailureReason(udid, settled.sim.state)
|
|
543
|
-
};
|
|
544
|
-
const root = sim.state === "Shutdown" ? void 0 : repoRootAbove(workspace);
|
|
545
|
-
if (root && settled.sim) closeOwnedDeviceSessions({
|
|
546
|
-
platform: "ios",
|
|
547
|
-
id: udid
|
|
548
|
-
}, () => resolveOwnedIosSim(udid).sim?.state === "Shutdown", workspace, root);
|
|
508
|
+
const binary = provider === "tailscale" ? resolveTailscaleBinary((bin) => getExecutor().findExecutable(bin)) : provider;
|
|
509
|
+
if (!binary) throw new Error("Install Tailscale and sign in to your tailnet.");
|
|
510
|
+
const { bin, args } = tunnelArgv(provider, port, ngrokUrl, outputFile, binary);
|
|
511
|
+
child = spawn(bin, args, {
|
|
512
|
+
stdio: outputFile ? [
|
|
513
|
+
"ignore",
|
|
514
|
+
"ignore",
|
|
515
|
+
"ignore"
|
|
516
|
+
] : [
|
|
517
|
+
"ignore",
|
|
518
|
+
"pipe",
|
|
519
|
+
"pipe"
|
|
520
|
+
],
|
|
521
|
+
detached: true
|
|
522
|
+
});
|
|
523
|
+
} catch (err) {
|
|
524
|
+
removeTunnelLogFile(outputFile);
|
|
549
525
|
return {
|
|
550
|
-
|
|
551
|
-
|
|
526
|
+
failed: true,
|
|
527
|
+
reason: `Could not start ${provider}: ${describe(err)}`
|
|
552
528
|
};
|
|
553
|
-
}
|
|
554
|
-
|
|
529
|
+
}
|
|
530
|
+
let childExited = false;
|
|
531
|
+
child.once("exit", () => {
|
|
532
|
+
childExited = true;
|
|
533
|
+
});
|
|
534
|
+
const cleanup = async (alreadyExited = childExited) => {
|
|
535
|
+
const stopped = await terminateChild(child, {
|
|
536
|
+
alreadyExited,
|
|
537
|
+
timeoutMs: cleanupTimeoutMs,
|
|
538
|
+
now,
|
|
539
|
+
sleep,
|
|
540
|
+
isAlive: isChildAlive
|
|
541
|
+
});
|
|
542
|
+
if (stopped) removeTunnelLogFile(outputFile);
|
|
543
|
+
return stopped ? { status: "stopped" } : {
|
|
555
544
|
status: "failed",
|
|
556
|
-
reason:
|
|
557
|
-
...parkFallback ? { parkFallback } : {}
|
|
545
|
+
reason: `Sent SIGKILL but could not confirm that pid ${child.pid ?? "unknown"} exited.`
|
|
558
546
|
};
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
assertOwnedAvdStopped(candidate.name, { resolveDirectory: () => candidate.directory });
|
|
575
|
-
const detached = withConfigLock$1(() => {
|
|
576
|
-
const current = lstatSync(candidate.directory);
|
|
577
|
-
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.`);
|
|
578
|
-
assertAvdReferences(candidate.name, owner);
|
|
579
|
-
const name = `stim-gc-${randomUUID()}`;
|
|
580
|
-
recordCreatedDevice("android", name);
|
|
581
|
-
const destination = join(dirname(candidate.directory), `${name}.avd`);
|
|
547
|
+
};
|
|
548
|
+
const failAfterCleanup = async (reason, alreadyExited = childExited) => {
|
|
549
|
+
const stopped = await cleanup(alreadyExited);
|
|
550
|
+
return stopped.status === "stopped" ? {
|
|
551
|
+
failed: true,
|
|
552
|
+
reason
|
|
553
|
+
} : {
|
|
554
|
+
failed: true,
|
|
555
|
+
reason: `${reason} ${stopped.reason}`,
|
|
556
|
+
cleanupFailed: true
|
|
557
|
+
};
|
|
558
|
+
};
|
|
559
|
+
const pid = child.pid;
|
|
560
|
+
if (!pid) return failAfterCleanup(`${provider} started but reported no pid.`);
|
|
561
|
+
const captureChildIdentity = () => {
|
|
582
562
|
try {
|
|
583
|
-
|
|
584
|
-
} catch
|
|
585
|
-
|
|
586
|
-
throw error;
|
|
563
|
+
return readProcessToken(pid);
|
|
564
|
+
} catch {
|
|
565
|
+
return null;
|
|
587
566
|
}
|
|
588
|
-
forgetCreatedDevice("android", candidate.name);
|
|
589
|
-
return destination;
|
|
590
|
-
});
|
|
591
|
-
const current = lstatSync(detached);
|
|
592
|
-
if (!current.isDirectory() || current.dev !== candidate.dev || current.ino !== candidate.ino) throw new Error(`Detached AVD data changed at ${detached}; it was kept.`);
|
|
593
|
-
try {
|
|
594
|
-
rmSync(detached, { recursive: true });
|
|
595
|
-
} catch (error) {
|
|
596
|
-
throw new Error(`Could not remove AVD data at ${detached}; retry gc --delete: ${error.message}`, { cause: error });
|
|
597
|
-
}
|
|
598
|
-
forgetCreatedDevice("android", basename(detached, ".avd"));
|
|
599
|
-
}
|
|
600
|
-
function teardownUnregisteredAvd(avdName, del, owner, surveyed) {
|
|
601
|
-
const candidates = surveyed ? [surveyed] : listOrphanedAvdDirectories(avdName);
|
|
602
|
-
if (candidates.length === 0) {
|
|
603
|
-
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.`);
|
|
604
|
-
return { status: "missing" };
|
|
605
|
-
}
|
|
606
|
-
if (!del) return {
|
|
607
|
-
status: "skipped",
|
|
608
|
-
reason: "AVD registration is missing; its data was kept."
|
|
609
567
|
};
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
568
|
+
const initialProcessToken = captureChildIdentity();
|
|
569
|
+
if (!initialProcessToken) return failAfterCleanup(`${provider} started but its process identity token could not be read.`);
|
|
570
|
+
const { url, exited, lastLine } = outputFile ? await waitForFileUrl(child, outputFile, parserFor(provider), urlTimeoutMs) : await waitForUrl(child, parserFor(provider), urlTimeoutMs);
|
|
571
|
+
if (!url) return failAfterCleanup(exited ? provider === "tailscale" && lastLine ? `${provider} exited before printing a tunnel URL: ${lastLine}` : `${provider} exited before printing a tunnel URL.` : `${provider} did not print a tunnel URL within ${urlTimeoutMs}ms.`, exited || childExited);
|
|
572
|
+
if (!outputFile) resumeChildPipes(child);
|
|
573
|
+
if (requireReachable) {
|
|
574
|
+
if (!await waitUntilReachable({
|
|
575
|
+
url,
|
|
576
|
+
now,
|
|
577
|
+
sleep,
|
|
578
|
+
probe: probeReachable,
|
|
579
|
+
timeoutMs: reachableTimeoutMs
|
|
580
|
+
})) return failAfterCleanup(`${url} did not become reachable within ${reachableTimeoutMs}ms; ${provider} may still be registering, or the network path is blocked.`);
|
|
613
581
|
}
|
|
582
|
+
const finalProcessToken = captureChildIdentity();
|
|
583
|
+
if (!finalProcessToken || finalProcessToken !== initialProcessToken) return failAfterCleanup(`${provider} process identity changed before its tunnel could be recorded.`);
|
|
584
|
+
if (!outputFile) unrefChildPipes(child);
|
|
585
|
+
child.unref?.();
|
|
586
|
+
if (childExited) return failAfterCleanup(`${provider} exited before its tunnel could be recorded.`, true);
|
|
614
587
|
return {
|
|
615
|
-
|
|
616
|
-
|
|
588
|
+
url,
|
|
589
|
+
pid,
|
|
590
|
+
processToken: initialProcessToken,
|
|
591
|
+
logFile: outputFile,
|
|
592
|
+
cleanup: () => cleanup()
|
|
617
593
|
};
|
|
618
594
|
}
|
|
619
|
-
function
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
595
|
+
async function startTunnelSequence({ providers, port, ngrokUrl = null, requireReachable = true, start = startTunnel }) {
|
|
596
|
+
const failures = [];
|
|
597
|
+
for (const provider of providers) {
|
|
598
|
+
const result = await start({
|
|
599
|
+
provider,
|
|
600
|
+
port,
|
|
601
|
+
ngrokUrl: provider === "ngrok" ? ngrokUrl : null,
|
|
602
|
+
requireReachable
|
|
603
|
+
});
|
|
604
|
+
if (!("failed" in result)) return {
|
|
605
|
+
provider,
|
|
606
|
+
...result
|
|
626
607
|
};
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
} catch (error) {
|
|
632
|
-
return {
|
|
633
|
-
status: "failed",
|
|
634
|
-
reason: String(error?.message || error)
|
|
608
|
+
failures.push(`${provider}: ${result.reason}`);
|
|
609
|
+
if (result.cleanupFailed) return {
|
|
610
|
+
failed: true,
|
|
611
|
+
reason: failures.join(" ")
|
|
635
612
|
};
|
|
636
|
-
} finally {
|
|
637
|
-
(0, ownership_claim_exports.releaseClaim)(claim);
|
|
638
613
|
}
|
|
614
|
+
return {
|
|
615
|
+
failed: true,
|
|
616
|
+
reason: failures.join(" ") || "No managed tunnel provider was selected."
|
|
617
|
+
};
|
|
639
618
|
}
|
|
640
|
-
function
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
...adb.emulators,
|
|
644
|
-
...adb.physical,
|
|
645
|
-
...adb.unhealthy
|
|
646
|
-
].some((entry) => entry.serial === serial);
|
|
619
|
+
function closeChildPipes(child) {
|
|
620
|
+
child.stdout?.destroy?.();
|
|
621
|
+
child.stderr?.destroy?.();
|
|
647
622
|
}
|
|
648
|
-
function
|
|
649
|
-
|
|
623
|
+
function unrefChildPipes(child) {
|
|
624
|
+
resumeChildPipes(child);
|
|
625
|
+
for (const stream of [child.stdout, child.stderr]) stream?.unref?.();
|
|
626
|
+
}
|
|
627
|
+
function resumeChildPipes(child) {
|
|
628
|
+
child.stdout?.resume?.();
|
|
629
|
+
child.stderr?.resume?.();
|
|
630
|
+
}
|
|
631
|
+
async function signalAndWaitForExit(child, signal, { timeoutMs, now, sleep, isAlive, platform }) {
|
|
632
|
+
let exited = false;
|
|
633
|
+
const onExit = () => {
|
|
634
|
+
exited = true;
|
|
635
|
+
};
|
|
636
|
+
child.once("exit", onExit);
|
|
650
637
|
try {
|
|
651
|
-
if (
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
|
|
657
|
-
};
|
|
658
|
-
if (resolved.missing) {
|
|
659
|
-
if (wipe) throw new Error(`AVD ${avdName} is not registered; its data was kept.`);
|
|
660
|
-
return teardownUnregisteredAvd(avdName, del, owner, orphanedDirectory);
|
|
661
|
-
}
|
|
662
|
-
if (onlyIfMissing) return {
|
|
663
|
-
status: "skipped",
|
|
664
|
-
reason: "AVD registration appeared; its record was kept."
|
|
665
|
-
};
|
|
666
|
-
if (orphanedDirectory) return {
|
|
667
|
-
status: "skipped",
|
|
668
|
-
reason: "AVD registration appeared; its data was kept."
|
|
669
|
-
};
|
|
670
|
-
const serial = resolved.serial;
|
|
671
|
-
const stillStopped = (sessionSerial) => {
|
|
672
|
-
const current = resolveAvd(avdName);
|
|
673
|
-
return !current.notOwned && !current.missing && !current.serial && serialFree(sessionSerial);
|
|
674
|
-
};
|
|
675
|
-
if (serial) {
|
|
676
|
-
const stillOwned = () => {
|
|
677
|
-
const current = resolveAvd(avdName);
|
|
678
|
-
return !current.notOwned && !current.missing && current.serial === serial;
|
|
679
|
-
};
|
|
680
|
-
closeOwnedDeviceSessions({
|
|
681
|
-
platform: "android",
|
|
682
|
-
id: serial,
|
|
683
|
-
avdName
|
|
684
|
-
}, stillOwned, workspace);
|
|
685
|
-
if (!stillOwned()) throw new Error(`Owned AVD ${avdName} changed before shutdown; it was kept.`);
|
|
686
|
-
waitForShutdown(avdName, (timeoutMs) => {
|
|
687
|
-
(0, ownership_claim_exports.markClaimChildPending)(claim);
|
|
688
|
-
try {
|
|
689
|
-
shutdownAndroidEmulator(serial, timeoutMs);
|
|
690
|
-
} finally {
|
|
691
|
-
(0, ownership_claim_exports.clearClaimChild)(claim);
|
|
692
|
-
}
|
|
693
|
-
});
|
|
694
|
-
const root = repoRootAbove(workspace);
|
|
695
|
-
if (root) {
|
|
696
|
-
const deadline = Date.now() + ADB_RELEASE_WAIT_MS;
|
|
697
|
-
while (!serialFree(serial) && Date.now() < deadline) sleepSync(250);
|
|
698
|
-
closeOwnedDeviceSessions({
|
|
699
|
-
platform: "android",
|
|
700
|
-
id: serial,
|
|
701
|
-
avdName
|
|
702
|
-
}, stillStopped, workspace, root);
|
|
703
|
-
}
|
|
704
|
-
} else {
|
|
705
|
-
closeOwnedDeviceSessions({
|
|
706
|
-
platform: "android",
|
|
707
|
-
id: null,
|
|
708
|
-
avdName
|
|
709
|
-
}, stillStopped, workspace);
|
|
710
|
-
waitForShutdown(avdName, null);
|
|
711
|
-
}
|
|
712
|
-
if (wipe && !del) {
|
|
713
|
-
const current = resolveAvd(avdName);
|
|
714
|
-
if (current.notOwned || current.missing) throw new Error(`AVD ${avdName} changed before its data was wiped.`);
|
|
715
|
-
if (current.serial) throw new Error(`Owned AVD ${avdName} started again before its data was wiped.`);
|
|
716
|
-
assertStopped(avdName);
|
|
717
|
-
withConfigLock$1(() => assertAvdReferences(avdName, owner));
|
|
718
|
-
const directory = ownedAvdDirectory(avdName);
|
|
719
|
-
if (!directory) throw new Error(`The data directory of AVD ${avdName} could not be resolved.`);
|
|
720
|
-
onWipeStart?.();
|
|
721
|
-
wipeAvdUserData(directory);
|
|
722
|
-
}
|
|
723
|
-
if (del) {
|
|
724
|
-
const current = resolveAvd(avdName);
|
|
725
|
-
if (current.notOwned) return {
|
|
726
|
-
status: "skipped",
|
|
727
|
-
kind: "not-owned",
|
|
728
|
-
reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
|
|
729
|
-
};
|
|
730
|
-
if (current.missing) return teardownUnregisteredAvd(avdName, del, owner);
|
|
731
|
-
if (current.serial) throw new Error(`Owned AVD ${avdName} started again before deletion.`);
|
|
732
|
-
assertStopped(avdName);
|
|
733
|
-
withConfigLock$1(() => assertAvdReferences(avdName, owner));
|
|
734
|
-
if (park && park.max > 0) try {
|
|
735
|
-
const systemImage = ownedAvdSystemImage(avdName);
|
|
736
|
-
if (!systemImage || !park.configuration || !ownedAvdMatchesConfiguration(avdName, park.configuration)) throw new Error("the AVD has no verified creation configuration");
|
|
737
|
-
const evicted = parkSim({
|
|
738
|
-
platform: "android",
|
|
739
|
-
projectPath: park.projectPath,
|
|
740
|
-
slot: park.slot,
|
|
741
|
-
max: park.max,
|
|
742
|
-
record: {
|
|
743
|
-
udid: avdName,
|
|
744
|
-
name: avdName,
|
|
745
|
-
systemImage,
|
|
746
|
-
configuration: park.configuration,
|
|
747
|
-
parkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
748
|
-
...park.appId ? { packageName: park.appId } : {},
|
|
749
|
-
...park.cacheKey ? { cacheKey: park.cacheKey } : {}
|
|
750
|
-
}
|
|
751
|
-
});
|
|
752
|
-
const removed = [];
|
|
753
|
-
const failures = [];
|
|
754
|
-
for (const entry of evicted) {
|
|
755
|
-
const result = teardownParkedAvd(entry.name);
|
|
756
|
-
if (result.status === "torn-down") removed.push({
|
|
757
|
-
udid: entry.name,
|
|
758
|
-
name: entry.name,
|
|
759
|
-
platform: "android"
|
|
760
|
-
});
|
|
761
|
-
else if (result.status === "failed") failures.push(`could not delete evicted ${entry.name}: ${result.reason}`);
|
|
762
|
-
}
|
|
763
|
-
return {
|
|
764
|
-
status: "torn-down",
|
|
765
|
-
label: avdName,
|
|
766
|
-
parked: {
|
|
767
|
-
udid: avdName,
|
|
768
|
-
name: avdName,
|
|
769
|
-
platform: "android"
|
|
770
|
-
},
|
|
771
|
-
evicted: removed,
|
|
772
|
-
...failures.length ? { evictionFailures: failures } : {}
|
|
773
|
-
};
|
|
774
|
-
} catch (error) {
|
|
775
|
-
parkFallback = String(error?.message || error);
|
|
776
|
-
}
|
|
777
|
-
const directory = ownedAvdDirectory(avdName);
|
|
778
|
-
const paths = avdStorageRoots().map((root) => join(root, `${avdName}.ini`));
|
|
779
|
-
if (directory) paths.push(directory);
|
|
780
|
-
(0, ownership_claim_exports.markClaimChildPending)(claim);
|
|
781
|
-
try {
|
|
782
|
-
deleteAvd(avdName);
|
|
783
|
-
} finally {
|
|
784
|
-
(0, ownership_claim_exports.clearClaimChild)(claim);
|
|
785
|
-
}
|
|
786
|
-
const remaining = paths.filter(avdPathExists);
|
|
787
|
-
if (remaining.length) throw new Error(`AVD ${avdName} was only partially deleted; data remains at ${remaining.join(", ")}.`);
|
|
788
|
-
}
|
|
789
|
-
return {
|
|
790
|
-
status: "torn-down",
|
|
791
|
-
label: avdName,
|
|
792
|
-
serial: resolved.serial ?? null,
|
|
793
|
-
...parkFallback ? { parkFallback } : {}
|
|
794
|
-
};
|
|
795
|
-
} catch (e) {
|
|
796
|
-
return {
|
|
797
|
-
status: "failed",
|
|
798
|
-
reason: String(e?.message || e)
|
|
799
|
-
};
|
|
638
|
+
if (platform === "win32" && child.pid) signalProcessTree(child.pid, signal, { platform });
|
|
639
|
+
child.kill(signal);
|
|
640
|
+
} catch (err) {
|
|
641
|
+
child.removeListener("exit", onExit);
|
|
642
|
+
return isEsrch(err);
|
|
800
643
|
}
|
|
644
|
+
const hasExited = () => exited;
|
|
645
|
+
const deadline = now() + timeoutMs;
|
|
646
|
+
while (!hasExited() && now() < deadline) await sleep(CLEANUP_POLL_MS);
|
|
647
|
+
if (!hasExited() && child.pid && !isAlive(child.pid)) exited = true;
|
|
648
|
+
child.removeListener("exit", onExit);
|
|
649
|
+
return hasExited();
|
|
801
650
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
651
|
+
async function terminateChild(child, { alreadyExited, timeoutMs, now, sleep, isAlive, platform = process.platform }) {
|
|
652
|
+
if (alreadyExited) {
|
|
653
|
+
closeChildPipes(child);
|
|
654
|
+
return true;
|
|
655
|
+
}
|
|
656
|
+
if (await signalAndWaitForExit(child, "SIGTERM", {
|
|
657
|
+
timeoutMs,
|
|
658
|
+
now,
|
|
659
|
+
sleep,
|
|
660
|
+
isAlive,
|
|
661
|
+
platform
|
|
662
|
+
})) {
|
|
663
|
+
closeChildPipes(child);
|
|
664
|
+
return true;
|
|
814
665
|
}
|
|
815
|
-
|
|
666
|
+
const killed = await signalAndWaitForExit(child, "SIGKILL", {
|
|
667
|
+
timeoutMs,
|
|
668
|
+
now,
|
|
669
|
+
sleep,
|
|
670
|
+
isAlive,
|
|
671
|
+
platform
|
|
672
|
+
});
|
|
673
|
+
closeChildPipes(child);
|
|
674
|
+
return killed;
|
|
816
675
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
676
|
+
function describe(err) {
|
|
677
|
+
return err?.message || String(err);
|
|
678
|
+
}
|
|
679
|
+
const STOP_TIMEOUT_MS = 5e3;
|
|
680
|
+
const STOP_POLL_MS = 100;
|
|
681
|
+
function isEsrch(err) {
|
|
682
|
+
return err?.code === "ESRCH";
|
|
683
|
+
}
|
|
684
|
+
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 } = {}) {
|
|
685
|
+
const missing = () => {
|
|
686
|
+
removeRecordedTunnelLogFile(record?.logFile);
|
|
687
|
+
return { status: "missing" };
|
|
688
|
+
};
|
|
689
|
+
const pid = record?.pid;
|
|
690
|
+
if (!pid || !isAlive(pid)) return missing();
|
|
691
|
+
if (!record.processToken) return {
|
|
692
|
+
status: "failed",
|
|
693
|
+
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.`
|
|
694
|
+
};
|
|
695
|
+
const identity = inspectIdentity(record);
|
|
696
|
+
if (identity === "gone" || identity === "different") return missing();
|
|
697
|
+
if (identity !== "same") return {
|
|
698
|
+
status: "failed",
|
|
699
|
+
reason: `could not verify the process identity for tunnel pid ${pid}; refusing to signal it.`
|
|
700
|
+
};
|
|
823
701
|
try {
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
});
|
|
828
|
-
} catch (error) {
|
|
829
|
-
return {
|
|
702
|
+
kill(pid);
|
|
703
|
+
} catch (err) {
|
|
704
|
+
return isEsrch(err) ? missing() : {
|
|
830
705
|
status: "failed",
|
|
831
|
-
reason:
|
|
706
|
+
reason: describe(err)
|
|
832
707
|
};
|
|
833
708
|
}
|
|
709
|
+
const deadline = now() + timeoutMs;
|
|
710
|
+
const exited = () => {
|
|
711
|
+
if (!isAlive(pid)) return true;
|
|
712
|
+
const current = inspectIdentity(record);
|
|
713
|
+
return current === "gone" || current === "different";
|
|
714
|
+
};
|
|
715
|
+
while (now() < deadline && !exited()) await sleep(STOP_POLL_MS);
|
|
716
|
+
if (!exited()) return {
|
|
717
|
+
status: "failed",
|
|
718
|
+
reason: `pid ${pid} did not exit within ${timeoutMs}ms.`
|
|
719
|
+
};
|
|
720
|
+
removeRecordedTunnelLogFile(record.logFile);
|
|
721
|
+
return { status: "stopped" };
|
|
834
722
|
}
|
|
835
|
-
|
|
836
|
-
|
|
723
|
+
//#endregion
|
|
724
|
+
//#region src/commands/gc/paths.ts
|
|
725
|
+
function canonicalPath(p) {
|
|
837
726
|
try {
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
const supervisor = (0, process_identity_exports.inspectProcessIdentity)(record);
|
|
842
|
-
const chrome = record.chromeProcess ? (0, process_identity_exports.inspectProcessIdentity)(record.chromeProcess) : "gone";
|
|
843
|
-
if (supervisor === "unknown" || chrome === "unknown") return {
|
|
844
|
-
status: "skipped",
|
|
845
|
-
kind: "not-verified",
|
|
846
|
-
reason: `the browser supervisor (pid ${record.pid}) or its Chrome could not be verified; its record was kept`
|
|
847
|
-
};
|
|
848
|
-
if (supervisor === "same") {
|
|
849
|
-
try {
|
|
850
|
-
process.kill(record.pid, "SIGTERM");
|
|
851
|
-
} catch {}
|
|
852
|
-
if (!await (0, process_identity_exports.waitForProcessExit)(record, BROWSER_EXIT_WAIT_MS)) return {
|
|
853
|
-
status: "failed",
|
|
854
|
-
reason: `browser supervisor pid ${record.pid} did not exit`
|
|
855
|
-
};
|
|
856
|
-
}
|
|
857
|
-
if (record.chromeProcess && !await stopOwnedChrome(record.chromeProcess)) return {
|
|
858
|
-
status: "failed",
|
|
859
|
-
reason: `Chrome pid ${record.chromeProcess.pid} did not exit`
|
|
860
|
-
};
|
|
861
|
-
const holder = liveProfileHolder(profile);
|
|
862
|
-
if (holder !== null) return {
|
|
863
|
-
status: "skipped",
|
|
864
|
-
kind: "not-verified",
|
|
865
|
-
reason: `pid ${holder} still holds ${profile}, and the browser record does not name it; it was left running`
|
|
866
|
-
};
|
|
867
|
-
const claims = (0, ownership_claim_exports.clearFreeClaimSet)({
|
|
868
|
-
root: webClaimRoot(root),
|
|
869
|
-
label: "browser supervisor"
|
|
870
|
-
});
|
|
871
|
-
if (claims.status !== "cleared") return {
|
|
872
|
-
status: "skipped",
|
|
873
|
-
kind: "not-verified",
|
|
874
|
-
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))}`
|
|
875
|
-
};
|
|
876
|
-
clearWebRecord(root, record);
|
|
877
|
-
await releaseBrowserPort(root, record.cdpPort);
|
|
878
|
-
} else {
|
|
879
|
-
const holder = liveProfileHolder(profile);
|
|
880
|
-
if (holder !== null) return {
|
|
881
|
-
status: "skipped",
|
|
882
|
-
kind: "not-verified",
|
|
883
|
-
reason: `pid ${holder} holds ${profile} but no browser supervisor is recorded; it was left running`
|
|
884
|
-
};
|
|
885
|
-
const port = getProject(root)?.ports?.[CDP_PORT_LABEL];
|
|
886
|
-
if (typeof port === "number") await releaseBrowserPort(root, port);
|
|
887
|
-
}
|
|
888
|
-
if (existsSync(profile)) removeSingletonFiles(profile);
|
|
889
|
-
if (deleteProfile && existsSync(profile)) {
|
|
890
|
-
if (!readCreatedDevices$1().web.has(profile)) return {
|
|
891
|
-
status: "skipped",
|
|
892
|
-
kind: "not-owned",
|
|
893
|
-
reason: `${profile} is not in Stim's ledger; it was kept`
|
|
894
|
-
};
|
|
895
|
-
rmSync(profile, {
|
|
896
|
-
recursive: true,
|
|
897
|
-
force: true
|
|
898
|
-
});
|
|
899
|
-
forgetCreatedDevice("web", profile);
|
|
900
|
-
}
|
|
901
|
-
return record ? {
|
|
902
|
-
status: "torn-down",
|
|
903
|
-
label: `Chrome (${profile})`
|
|
904
|
-
} : { status: "missing" };
|
|
905
|
-
} catch (error) {
|
|
906
|
-
return {
|
|
907
|
-
status: "failed",
|
|
908
|
-
reason: String(error?.message || error)
|
|
909
|
-
};
|
|
727
|
+
return realpathSync$1(p);
|
|
728
|
+
} catch {
|
|
729
|
+
return resolve$1(p);
|
|
910
730
|
}
|
|
911
731
|
}
|
|
912
732
|
//#endregion
|
|
913
|
-
//#region src/
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
else timer = setTimeout(() => reject(/* @__PURE__ */ new Error("DNS lookup timed out")), DNS_TIMEOUT_MS);
|
|
733
|
+
//#region src/commands/gc/results.ts
|
|
734
|
+
const results = [];
|
|
735
|
+
function recordGcResult(kind, status, label, { id = null, bytes = null, detail = null } = {}) {
|
|
736
|
+
results.push({
|
|
737
|
+
kind,
|
|
738
|
+
status,
|
|
739
|
+
label,
|
|
740
|
+
id,
|
|
741
|
+
bytes,
|
|
742
|
+
detail
|
|
924
743
|
});
|
|
925
|
-
try {
|
|
926
|
-
return await Promise.race([resolve4(hostname), unavailable]);
|
|
927
|
-
} finally {
|
|
928
|
-
clearTimeout(timer);
|
|
929
|
-
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
930
|
-
}
|
|
931
744
|
}
|
|
932
|
-
|
|
745
|
+
function takeGcResults() {
|
|
746
|
+
return results.splice(0);
|
|
747
|
+
}
|
|
748
|
+
//#endregion
|
|
749
|
+
//#region src/devices/agent-device-cleanup.ts
|
|
750
|
+
function parseAgentDeviceSessions(output) {
|
|
751
|
+
const result = JSON.parse(output);
|
|
752
|
+
if (result?.success !== true || !Array.isArray(result.data?.sessions)) throw new Error("agent-device did not return a successful session list");
|
|
753
|
+
return result.data.sessions.flatMap((entry) => {
|
|
754
|
+
if (!entry || typeof entry !== "object") return [];
|
|
755
|
+
const row = entry;
|
|
756
|
+
const id = row.platform === "ios" ? row.device_udid : row.id;
|
|
757
|
+
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 [];
|
|
758
|
+
return [{
|
|
759
|
+
name: typeof row.address === "string" && row.address.trim() ? row.address : row.name,
|
|
760
|
+
platform: row.platform,
|
|
761
|
+
id,
|
|
762
|
+
device: typeof row.device === "string" ? row.device : null,
|
|
763
|
+
createdAt: row.createdAt
|
|
764
|
+
}];
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
function isOwnDeviceSession(session, device, owner) {
|
|
768
|
+
if (session.platform !== device.platform || device.id !== null && session.id !== device.id) return false;
|
|
769
|
+
if (device.platform === "android" && session.device !== device.avdName.replaceAll("_", " ")) return false;
|
|
770
|
+
if (!owner) return true;
|
|
771
|
+
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));
|
|
772
|
+
}
|
|
773
|
+
function within(root, path) {
|
|
774
|
+
const rest = relative(root, path);
|
|
775
|
+
return rest === "" || !isAbsolute(rest) && rest.split(sep)[0] !== "..";
|
|
776
|
+
}
|
|
777
|
+
function canonical(path) {
|
|
933
778
|
try {
|
|
934
|
-
return (
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
})).status;
|
|
938
|
-
} catch (error) {
|
|
939
|
-
const cause = error?.cause;
|
|
940
|
-
if (cause?.code !== "ENOTFOUND" && cause?.code !== "EAI_AGAIN") return null;
|
|
779
|
+
return realpathSync(path);
|
|
780
|
+
} catch {
|
|
781
|
+
return null;
|
|
941
782
|
}
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
783
|
+
}
|
|
784
|
+
function deviceClaims(device) {
|
|
785
|
+
return readAgentDeviceRecords(homedir()).filter((record) => record.kind === "claim" && (device.id === null || record.deviceId === device.id)).map((record) => ({
|
|
786
|
+
deviceId: record.deviceId,
|
|
787
|
+
session: record.session,
|
|
788
|
+
workspace: record.workspace && canonical(record.workspace)
|
|
789
|
+
}));
|
|
790
|
+
}
|
|
791
|
+
function explainFailure(error) {
|
|
792
|
+
const stdout = error?.stdout;
|
|
793
|
+
let failure;
|
|
945
794
|
try {
|
|
946
|
-
|
|
795
|
+
failure = typeof stdout === "string" ? JSON.parse(stdout)?.error : void 0;
|
|
947
796
|
} catch {
|
|
948
|
-
|
|
797
|
+
failure = void 0;
|
|
949
798
|
}
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
return new
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
799
|
+
if (typeof failure?.message !== "string" || !failure.message) return error;
|
|
800
|
+
const reason = typeof failure.details?.reason === "string" ? ` (${failure.details.reason})` : "";
|
|
801
|
+
return /* @__PURE__ */ new Error(`agent-device refused: ${failure.message}${reason}`);
|
|
802
|
+
}
|
|
803
|
+
function report(message) {
|
|
804
|
+
process.stderr.write(`${phaseLine("device", message)}\n`);
|
|
805
|
+
}
|
|
806
|
+
function closeOwnedDeviceSessions(device, stillOwned, workspace, repoRoot) {
|
|
807
|
+
const exec = getExecutor();
|
|
808
|
+
try {
|
|
809
|
+
if (!exec.findExecutable("agent-device")) return;
|
|
810
|
+
const root = workspace === void 0 ? void 0 : canonical(workspace);
|
|
811
|
+
if (root === null) return;
|
|
812
|
+
const owner = root === void 0 ? void 0 : {
|
|
813
|
+
workspace: root,
|
|
814
|
+
repoRoot: repoRoot && (canonical(repoRoot) ?? void 0),
|
|
815
|
+
claims: deviceClaims(device)
|
|
816
|
+
};
|
|
817
|
+
const stateDir = root === void 0 ? void 0 : workspaceAgentDeviceDir(root);
|
|
818
|
+
const stateDirs = [void 0, ...stateDir && !["none", "dead"].includes(agentDeviceDaemon(stateDir)) ? [stateDir] : []];
|
|
819
|
+
for (const dir of stateDirs) {
|
|
820
|
+
const deadline = Date.now() + 15e3;
|
|
821
|
+
const run = (args) => {
|
|
822
|
+
const remaining = deadline - Date.now();
|
|
823
|
+
if (remaining <= 0) throw new Error("agent-device cleanup timed out");
|
|
824
|
+
try {
|
|
825
|
+
return exec.runFile("agent-device", [
|
|
826
|
+
...args,
|
|
827
|
+
...dir ? ["--state-dir", dir] : [],
|
|
828
|
+
"--json",
|
|
829
|
+
"--daemon-transport",
|
|
830
|
+
"socket"
|
|
831
|
+
], {
|
|
832
|
+
timeoutMs: Math.min(5e3, remaining),
|
|
833
|
+
killSignal: "SIGKILL"
|
|
834
|
+
});
|
|
835
|
+
} catch (error) {
|
|
836
|
+
throw explainFailure(error);
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
const list = () => parseAgentDeviceSessions(run([
|
|
840
|
+
"session",
|
|
841
|
+
"list",
|
|
842
|
+
"--session",
|
|
843
|
+
"stim-teardown-inventory"
|
|
844
|
+
]));
|
|
845
|
+
try {
|
|
846
|
+
const sessions = list().filter((session) => isOwnDeviceSession(session, device, owner));
|
|
847
|
+
for (const session of sessions) try {
|
|
848
|
+
if (!list().some((current) => current.name === session.name && current.platform === session.platform && current.id === session.id && current.createdAt === session.createdAt)) continue;
|
|
849
|
+
if (!stillOwned(session.id)) continue;
|
|
850
|
+
if (JSON.parse(run([
|
|
851
|
+
"close",
|
|
852
|
+
"--session",
|
|
853
|
+
session.name,
|
|
854
|
+
"--session-lock",
|
|
855
|
+
"reject",
|
|
856
|
+
"--platform",
|
|
857
|
+
device.platform,
|
|
858
|
+
device.platform === "ios" ? "--udid" : "--serial",
|
|
859
|
+
session.id
|
|
860
|
+
]))?.success !== true) throw new Error("agent-device did not confirm session close");
|
|
861
|
+
report(`closed agent-device session ${session.name} on ${session.id}`);
|
|
862
|
+
} catch (error) {
|
|
863
|
+
report(`could not close agent-device session ${session.name} on ${session.id}: ${error.message}`);
|
|
864
|
+
}
|
|
865
|
+
} catch (error) {
|
|
866
|
+
report(`could not list agent-device sessions for ${device.platform === "ios" ? device.id : device.id ?? device.avdName}: ${error.message}`);
|
|
961
867
|
}
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
request$1.on("error", () => resolve(null));
|
|
967
|
-
request$1.end();
|
|
968
|
-
});
|
|
868
|
+
}
|
|
869
|
+
} catch (error) {
|
|
870
|
+
report(`could not list agent-device sessions for ${device.platform === "ios" ? device.id : device.id ?? device.avdName}: ${error.message}`);
|
|
871
|
+
}
|
|
969
872
|
}
|
|
970
873
|
//#endregion
|
|
971
|
-
//#region src/
|
|
972
|
-
function
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
"
|
|
985
|
-
"
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
String(port),
|
|
995
|
-
`--log=${logFile || "stdout"}`,
|
|
996
|
-
"--log-format=json",
|
|
997
|
-
...ngrokUrl ? ["--url", ngrokUrl] : []
|
|
998
|
-
]
|
|
999
|
-
};
|
|
1000
|
-
}
|
|
1001
|
-
const CLOUDFLARED_URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/i;
|
|
1002
|
-
function parseCloudflaredLine(line) {
|
|
1003
|
-
const match = line.match(CLOUDFLARED_URL_RE);
|
|
1004
|
-
return match ? match[0] : null;
|
|
874
|
+
//#region src/devices/teardown.ts
|
|
875
|
+
function teardownParkedAvd(avdName) {
|
|
876
|
+
try {
|
|
877
|
+
return removeParkedAfter("android", avdName, () => {
|
|
878
|
+
const result = teardownOwnedAvd(avdName, {
|
|
879
|
+
del: true,
|
|
880
|
+
owner: { pool: true }
|
|
881
|
+
});
|
|
882
|
+
if (result.status !== "torn-down" && result.status !== "missing") throw new Error(result.reason);
|
|
883
|
+
}) ? {
|
|
884
|
+
status: "torn-down",
|
|
885
|
+
label: avdName
|
|
886
|
+
} : {
|
|
887
|
+
status: "skipped",
|
|
888
|
+
kind: "not-parked",
|
|
889
|
+
reason: "emulator is no longer parked"
|
|
890
|
+
};
|
|
891
|
+
} catch (error) {
|
|
892
|
+
return {
|
|
893
|
+
status: "failed",
|
|
894
|
+
reason: String(error?.message || error)
|
|
895
|
+
};
|
|
896
|
+
}
|
|
1005
897
|
}
|
|
1006
|
-
function
|
|
1007
|
-
let data;
|
|
898
|
+
function teardownParkedIosSim(udid, { label, deleteSim = deleteParkedIosSim } = {}) {
|
|
1008
899
|
try {
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
900
|
+
const removed = removeParkedAfter("ios", udid, () => {
|
|
901
|
+
closeOwnedDeviceSessions({
|
|
902
|
+
platform: "ios",
|
|
903
|
+
id: udid
|
|
904
|
+
}, () => Boolean(resolveOwnedIosSim(udid).sim));
|
|
905
|
+
deleteSim(udid);
|
|
906
|
+
});
|
|
907
|
+
if (!removed) return {
|
|
908
|
+
status: "skipped",
|
|
909
|
+
kind: "not-parked",
|
|
910
|
+
reason: "simulator is no longer parked"
|
|
911
|
+
};
|
|
912
|
+
return {
|
|
913
|
+
status: "torn-down",
|
|
914
|
+
label: label ?? removed.name
|
|
915
|
+
};
|
|
916
|
+
} catch (error) {
|
|
917
|
+
return {
|
|
918
|
+
status: "failed",
|
|
919
|
+
reason: String(error?.message || error)
|
|
920
|
+
};
|
|
1012
921
|
}
|
|
1013
|
-
if (typeof data !== "object" || data === null) return null;
|
|
1014
|
-
const url = data.url;
|
|
1015
|
-
return typeof url === "string" && /^https?:\/\//i.test(url) ? url : null;
|
|
1016
922
|
}
|
|
1017
|
-
function
|
|
1018
|
-
|
|
923
|
+
function eraseParkedIosSim(udid, { label } = {}) {
|
|
924
|
+
try {
|
|
925
|
+
const erased = eraseParkedAfter("ios", udid, (_record, eraseStarted) => {
|
|
926
|
+
closeOwnedDeviceSessions({
|
|
927
|
+
platform: "ios",
|
|
928
|
+
id: udid
|
|
929
|
+
}, () => Boolean(resolveOwnedIosSim(udid).sim));
|
|
930
|
+
const resolved = resolveOwnedIosSim(udid);
|
|
931
|
+
if (resolved.missing) throw new Error(`simulator ${udid} is not on this machine`);
|
|
932
|
+
if (resolved.notOwned) throw new Error(`simulator ${udid} is now named ${JSON.stringify(resolved.notOwned)}`);
|
|
933
|
+
const state = resolved.sim.state;
|
|
934
|
+
if (state !== "Shutdown") throw new Error(`simulator ${udid} is ${state}, not Shutdown; it was kept`);
|
|
935
|
+
eraseStarted();
|
|
936
|
+
eraseIosSim(udid);
|
|
937
|
+
});
|
|
938
|
+
if (!erased) return {
|
|
939
|
+
status: "skipped",
|
|
940
|
+
kind: "not-parked",
|
|
941
|
+
reason: "simulator is no longer parked"
|
|
942
|
+
};
|
|
943
|
+
return {
|
|
944
|
+
status: "torn-down",
|
|
945
|
+
label: label ?? erased.name
|
|
946
|
+
};
|
|
947
|
+
} catch (error) {
|
|
948
|
+
return {
|
|
949
|
+
status: "failed",
|
|
950
|
+
reason: String(error?.message || error)
|
|
951
|
+
};
|
|
952
|
+
}
|
|
1019
953
|
}
|
|
1020
|
-
function
|
|
1021
|
-
|
|
1022
|
-
|
|
954
|
+
function eraseParkedAvd(avdName) {
|
|
955
|
+
try {
|
|
956
|
+
return eraseParkedAfter("android", avdName, (_record, eraseStarted) => {
|
|
957
|
+
if (resolveOwnedAvdSerial(avdName, { timeoutMs: AVD_INVENTORY_TIMEOUT_MS }).serial) throw new Error(`AVD ${avdName} is running; it was kept`);
|
|
958
|
+
const result = teardownOwnedAvd(avdName, {
|
|
959
|
+
wipe: true,
|
|
960
|
+
owner: { pool: true },
|
|
961
|
+
onWipeStart: eraseStarted
|
|
962
|
+
});
|
|
963
|
+
if (result.status !== "torn-down") throw new Error(result.reason ?? `AVD ${avdName} is ${result.status}`);
|
|
964
|
+
}) ? {
|
|
965
|
+
status: "torn-down",
|
|
966
|
+
label: avdName
|
|
967
|
+
} : {
|
|
968
|
+
status: "skipped",
|
|
969
|
+
kind: "not-parked",
|
|
970
|
+
reason: "emulator is no longer parked"
|
|
971
|
+
};
|
|
972
|
+
} catch (error) {
|
|
973
|
+
return {
|
|
974
|
+
status: "failed",
|
|
975
|
+
reason: String(error?.message || error)
|
|
976
|
+
};
|
|
977
|
+
}
|
|
1023
978
|
}
|
|
1024
|
-
const
|
|
1025
|
-
const
|
|
1026
|
-
const
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
}
|
|
979
|
+
const IOS_SHUTDOWN_SETTLE_MS = 15e3;
|
|
980
|
+
const ADB_RELEASE_WAIT_MS = 3e3;
|
|
981
|
+
const AVD_INVENTORY_TIMEOUT_MS = 15e3;
|
|
982
|
+
function repoRootAbove(workspace) {
|
|
983
|
+
if (workspace === void 0 || !getExecutor().findExecutable("agent-device")) return void 0;
|
|
984
|
+
const root = repoRoot(workspace);
|
|
985
|
+
try {
|
|
986
|
+
return root && realpathSync(root) !== realpathSync(workspace) ? root : void 0;
|
|
987
|
+
} catch {
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
1033
990
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
async function withManagedRemoteWorktreeRemovalLock(worktreeRoot, fn, options = {}) {
|
|
1048
|
-
return withWorkspaceProcessLock(managedRemoteWorktreeLockRoot(worktreeRoot), "managed-remote", fn, {
|
|
1049
|
-
...options,
|
|
1050
|
-
external: true,
|
|
1051
|
-
ownerPurpose: "worktree removal"
|
|
1052
|
-
});
|
|
1053
|
-
}
|
|
1054
|
-
function managedLockHolders(root) {
|
|
1055
|
-
const locks = [{
|
|
1056
|
-
label: "managed tunnel",
|
|
1057
|
-
path: workspaceProcessLockPath(managedTunnelLockRoot(root), "metro-tunnel", false)
|
|
1058
|
-
}, {
|
|
1059
|
-
label: "managed remote",
|
|
1060
|
-
path: workspaceProcessLockPath(managedRemoteWorktreeLockRoot(root), "managed-remote", true)
|
|
1061
|
-
}];
|
|
1062
|
-
const reasons = [];
|
|
1063
|
-
for (const { label, path } of locks) {
|
|
1064
|
-
const claims = (0, ownership_claim_exports.readClaimSet)(path);
|
|
1065
|
-
const holder = claims.live[0];
|
|
1066
|
-
if (holder) {
|
|
1067
|
-
const purpose = typeof holder.details.purpose === "string" ? ` for ${holder.details.purpose}` : "";
|
|
1068
|
-
reasons.push(`the ${label} lock is held${purpose}`);
|
|
1069
|
-
} else if (claims.unresolved[0]) reasons.push(`the ${label} lock cannot be resolved: ${claims.unresolved[0].reason}`);
|
|
991
|
+
const IOS_SHUTDOWN_POLL_MS = 250;
|
|
992
|
+
const IOS_SHUTDOWN_ATTEMPTS = 2;
|
|
993
|
+
function settleIosSimShutdown(udid, { now = Date.now, sleep = sleepSync }) {
|
|
994
|
+
let resolved = resolveOwnedIosSim(udid);
|
|
995
|
+
for (let attempt = 1;; attempt++) {
|
|
996
|
+
const deadline = now() + IOS_SHUTDOWN_SETTLE_MS;
|
|
997
|
+
while (resolved.sim && resolved.sim.state !== "Shutdown" && now() < deadline) {
|
|
998
|
+
sleep(IOS_SHUTDOWN_POLL_MS);
|
|
999
|
+
resolved = resolveOwnedIosSim(udid);
|
|
1000
|
+
}
|
|
1001
|
+
if (!resolved.sim || resolved.sim.state === "Shutdown" || attempt === IOS_SHUTDOWN_ATTEMPTS) return resolved;
|
|
1002
|
+
shutdownIosSim(udid);
|
|
1003
|
+
resolved = resolveOwnedIosSim(udid);
|
|
1070
1004
|
}
|
|
1071
|
-
return reasons;
|
|
1072
|
-
}
|
|
1073
|
-
function managedTunnelLockRoot(root) {
|
|
1074
|
-
const key = createHash("sha256").update(resolve(root)).digest("hex");
|
|
1075
|
-
return join(getConfigDir(), "process-locks", key);
|
|
1076
1005
|
}
|
|
1077
|
-
function
|
|
1078
|
-
|
|
1079
|
-
return join(getConfigDir(), "process-locks", "worktrees", key);
|
|
1006
|
+
function iosShutdownFailureReason(udid, state) {
|
|
1007
|
+
return `simulator ${udid} is still ${state} after ${IOS_SHUTDOWN_ATTEMPTS} shutdown attempts and ${IOS_SHUTDOWN_ATTEMPTS * IOS_SHUTDOWN_SETTLE_MS / 1e3}s of waiting`;
|
|
1080
1008
|
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1009
|
+
function parkOwnedIosSim(settled, udid, park) {
|
|
1010
|
+
if (settled.missing) throw new Error(`simulator ${udid} disappeared after shutdown`);
|
|
1011
|
+
if (settled.notOwned) throw new Error(`simulator ${udid} is now named ${JSON.stringify(settled.notOwned)}`);
|
|
1012
|
+
const sim = settled.sim;
|
|
1013
|
+
if (sim.state !== "Shutdown") throw new Error(iosShutdownFailureReason(udid, sim.state));
|
|
1014
|
+
const model = listIosDeviceTypes().find((d) => d.identifier === sim.deviceTypeIdentifier)?.name ?? null;
|
|
1015
|
+
const runtime = parseRuntimeVersion(sim.runtime);
|
|
1016
|
+
const name = parkedSimName(sim.udid, {
|
|
1017
|
+
model,
|
|
1018
|
+
runtime
|
|
1019
|
+
});
|
|
1020
|
+
renameIosSim(sim.udid, name);
|
|
1021
|
+
const record = {
|
|
1022
|
+
udid: sim.udid,
|
|
1023
|
+
name,
|
|
1024
|
+
deviceTypeIdentifier: sim.deviceTypeIdentifier,
|
|
1025
|
+
runtimeIdentifier: sim.runtime,
|
|
1026
|
+
parkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1027
|
+
simslimManaged: Boolean(park.simslimManaged),
|
|
1028
|
+
...park.appId ? { bundleId: park.appId } : {},
|
|
1029
|
+
...park.cacheKey ? { cacheKey: park.cacheKey } : {}
|
|
1030
|
+
};
|
|
1031
|
+
return {
|
|
1032
|
+
record,
|
|
1033
|
+
evicted: parkSim({
|
|
1034
|
+
platform: "ios",
|
|
1035
|
+
projectPath: park.projectPath,
|
|
1036
|
+
slot: park.slot,
|
|
1037
|
+
record,
|
|
1038
|
+
max: park.max
|
|
1039
|
+
})
|
|
1040
|
+
};
|
|
1084
1041
|
}
|
|
1085
|
-
function
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
let onExit;
|
|
1094
|
-
const finish = (url, exited = false) => {
|
|
1095
|
-
if (settled) return;
|
|
1096
|
-
settled = true;
|
|
1097
|
-
clearTimeout(timer);
|
|
1098
|
-
child.stdout?.removeListener("data", onOut);
|
|
1099
|
-
child.stderr?.removeListener("data", onErr);
|
|
1100
|
-
child.removeListener("error", onError);
|
|
1101
|
-
child.removeListener("exit", onExit);
|
|
1102
|
-
resolve({
|
|
1103
|
-
url,
|
|
1104
|
-
exited,
|
|
1105
|
-
lastLine
|
|
1106
|
-
});
|
|
1042
|
+
function teardownOwnedIosSim(udid, { del = false, label, park, workspace, shutdownClock = {} } = {}) {
|
|
1043
|
+
let parkFallback;
|
|
1044
|
+
try {
|
|
1045
|
+
const resolved = resolveOwnedIosSim(udid);
|
|
1046
|
+
if (resolved.notOwned) return {
|
|
1047
|
+
status: "skipped",
|
|
1048
|
+
kind: "not-owned",
|
|
1049
|
+
reason: `sim "${resolved.notOwned}" is not Stim-owned: Stim has no record of creating it`
|
|
1107
1050
|
};
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1051
|
+
if (resolved.missing) return { status: "missing" };
|
|
1052
|
+
closeOwnedDeviceSessions({
|
|
1053
|
+
platform: "ios",
|
|
1054
|
+
id: udid
|
|
1055
|
+
}, () => Boolean(resolveOwnedIosSim(udid).sim), workspace);
|
|
1056
|
+
const current = resolveOwnedIosSim(udid);
|
|
1057
|
+
if (current.missing) return { status: "missing" };
|
|
1058
|
+
if (current.notOwned) return {
|
|
1059
|
+
status: "skipped",
|
|
1060
|
+
kind: "not-owned",
|
|
1061
|
+
reason: "simulator ownership changed before shutdown"
|
|
1113
1062
|
};
|
|
1114
|
-
|
|
1115
|
-
const
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1063
|
+
shutdownIosSim(udid);
|
|
1064
|
+
const sim = resolved.sim;
|
|
1065
|
+
if (del && park && park.max > 0) try {
|
|
1066
|
+
const { record, evicted } = parkOwnedIosSim(settleIosSimShutdown(udid, shutdownClock), udid, park);
|
|
1067
|
+
const removed = [];
|
|
1068
|
+
const failures = [];
|
|
1069
|
+
for (const entry of evicted) {
|
|
1070
|
+
const result = teardownParkedIosSim(entry.udid, { label: entry.name });
|
|
1071
|
+
if (result.status === "torn-down") removed.push({
|
|
1072
|
+
udid: entry.udid,
|
|
1073
|
+
name: entry.name
|
|
1074
|
+
});
|
|
1075
|
+
else if (result.status === "failed") failures.push(`could not delete evicted ${entry.name} (${entry.udid}): ${result.reason}`);
|
|
1076
|
+
}
|
|
1077
|
+
return {
|
|
1078
|
+
status: "torn-down",
|
|
1079
|
+
label: label ?? sim.name ?? udid,
|
|
1080
|
+
parked: {
|
|
1081
|
+
udid: record.udid,
|
|
1082
|
+
name: record.name
|
|
1083
|
+
},
|
|
1084
|
+
evicted: removed,
|
|
1085
|
+
...failures.length ? { evictionFailures: failures } : {}
|
|
1086
|
+
};
|
|
1087
|
+
} catch (e) {
|
|
1088
|
+
parkFallback = String(e?.message || e);
|
|
1089
|
+
}
|
|
1090
|
+
if (del) {
|
|
1091
|
+
deleteIosSim(udid);
|
|
1092
|
+
return {
|
|
1093
|
+
status: "torn-down",
|
|
1094
|
+
label: label ?? sim.name ?? udid,
|
|
1095
|
+
...parkFallback ? { parkFallback } : {}
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
const settled = settleIosSimShutdown(udid, shutdownClock);
|
|
1099
|
+
if (settled.sim && settled.sim.state !== "Shutdown") return {
|
|
1100
|
+
status: "failed",
|
|
1101
|
+
label: label ?? sim.name ?? udid,
|
|
1102
|
+
reason: iosShutdownFailureReason(udid, settled.sim.state)
|
|
1123
1103
|
};
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
}
|
|
1133
|
-
function waitForFileUrl(child, logFile, parseLine, timeoutMs) {
|
|
1134
|
-
return new Promise((resolve) => {
|
|
1135
|
-
let settled = false;
|
|
1136
|
-
const finish = (url, exited = false) => {
|
|
1137
|
-
if (settled) return;
|
|
1138
|
-
settled = true;
|
|
1139
|
-
clearInterval(interval);
|
|
1140
|
-
clearTimeout(timer);
|
|
1141
|
-
child.removeListener("error", onError);
|
|
1142
|
-
child.removeListener("exit", onExit);
|
|
1143
|
-
resolve({
|
|
1144
|
-
url,
|
|
1145
|
-
exited
|
|
1146
|
-
});
|
|
1104
|
+
const root = sim.state === "Shutdown" ? void 0 : repoRootAbove(workspace);
|
|
1105
|
+
if (root && settled.sim) closeOwnedDeviceSessions({
|
|
1106
|
+
platform: "ios",
|
|
1107
|
+
id: udid
|
|
1108
|
+
}, () => resolveOwnedIosSim(udid).sim?.state === "Shutdown", workspace, root);
|
|
1109
|
+
return {
|
|
1110
|
+
status: "torn-down",
|
|
1111
|
+
label: label ?? sim.name ?? udid
|
|
1147
1112
|
};
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
}
|
|
1154
|
-
} catch {}
|
|
1113
|
+
} catch (e) {
|
|
1114
|
+
return {
|
|
1115
|
+
status: "failed",
|
|
1116
|
+
reason: String(e?.message || e),
|
|
1117
|
+
...parkFallback ? { parkFallback } : {}
|
|
1155
1118
|
};
|
|
1156
|
-
|
|
1157
|
-
const onExit = () => finish(null, true);
|
|
1158
|
-
child.on("error", onError);
|
|
1159
|
-
child.on("exit", onExit);
|
|
1160
|
-
const interval = setInterval(probe, 25);
|
|
1161
|
-
const timer = setTimeout(() => finish(null), timeoutMs);
|
|
1162
|
-
probe();
|
|
1163
|
-
});
|
|
1119
|
+
}
|
|
1164
1120
|
}
|
|
1165
|
-
function
|
|
1166
|
-
const
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1121
|
+
function assertAvdReferences(avdName, owner) {
|
|
1122
|
+
const config = loadConfig$1();
|
|
1123
|
+
if (!config) throw new Error("Cannot verify AVD references without a Stim config; the data was kept.");
|
|
1124
|
+
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.`);
|
|
1125
|
+
for (const [projectPath, project] of Object.entries(config.projects)) for (const { slot, platforms } of projectDeviceSlots(project)) {
|
|
1126
|
+
if (platforms.android?.avdName?.toLowerCase() !== avdName.toLowerCase()) continue;
|
|
1127
|
+
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.`);
|
|
1128
|
+
}
|
|
1129
|
+
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.`);
|
|
1171
1130
|
}
|
|
1172
|
-
function
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1131
|
+
function removeOrphanedAvdDirectory(candidate, owner) {
|
|
1132
|
+
const surveyed = listOrphanedAvdDirectories(candidate.name).find((entry) => entry.directory === candidate.directory);
|
|
1133
|
+
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.`);
|
|
1134
|
+
assertOwnedAvdStopped(candidate.name, { resolveDirectory: () => candidate.directory });
|
|
1135
|
+
const detached = withConfigLock$1(() => {
|
|
1136
|
+
const current = lstatSync(candidate.directory);
|
|
1137
|
+
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.`);
|
|
1138
|
+
assertAvdReferences(candidate.name, owner);
|
|
1139
|
+
const name = `stim-gc-${randomUUID()}`;
|
|
1140
|
+
recordCreatedDevice("android", name);
|
|
1141
|
+
const destination = join(dirname(candidate.directory), `${name}.avd`);
|
|
1142
|
+
try {
|
|
1143
|
+
renameSync(candidate.directory, destination);
|
|
1144
|
+
} catch (error) {
|
|
1145
|
+
forgetCreatedDevice("android", name);
|
|
1146
|
+
throw error;
|
|
1147
|
+
}
|
|
1148
|
+
forgetCreatedDevice("android", candidate.name);
|
|
1149
|
+
return destination;
|
|
1150
|
+
});
|
|
1151
|
+
const current = lstatSync(detached);
|
|
1152
|
+
if (!current.isDirectory() || current.dev !== candidate.dev || current.ino !== candidate.ino) throw new Error(`Detached AVD data changed at ${detached}; it was kept.`);
|
|
1153
|
+
try {
|
|
1154
|
+
rmSync(detached, { recursive: true });
|
|
1155
|
+
} catch (error) {
|
|
1156
|
+
throw new Error(`Could not remove AVD data at ${detached}; retry gc --delete: ${error.message}`, { cause: error });
|
|
1157
|
+
}
|
|
1158
|
+
forgetCreatedDevice("android", basename(detached, ".avd"));
|
|
1184
1159
|
}
|
|
1185
|
-
|
|
1186
|
-
const
|
|
1187
|
-
|
|
1188
|
-
if (
|
|
1189
|
-
|
|
1190
|
-
await sleep(REACHABLE_POLL_MS);
|
|
1160
|
+
function teardownUnregisteredAvd(avdName, del, owner, surveyed) {
|
|
1161
|
+
const candidates = surveyed ? [surveyed] : listOrphanedAvdDirectories(avdName);
|
|
1162
|
+
if (candidates.length === 0) {
|
|
1163
|
+
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.`);
|
|
1164
|
+
return { status: "missing" };
|
|
1191
1165
|
}
|
|
1166
|
+
if (!del) return {
|
|
1167
|
+
status: "skipped",
|
|
1168
|
+
reason: "AVD registration is missing; its data was kept."
|
|
1169
|
+
};
|
|
1170
|
+
for (const candidate of candidates) {
|
|
1171
|
+
if (candidate.name !== avdName) throw new Error(`AVD data does not match ${avdName}; it was kept.`);
|
|
1172
|
+
removeOrphanedAvdDirectory(candidate, owner);
|
|
1173
|
+
}
|
|
1174
|
+
return {
|
|
1175
|
+
status: "torn-down",
|
|
1176
|
+
label: avdName
|
|
1177
|
+
};
|
|
1192
1178
|
}
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
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 }) {
|
|
1196
|
-
const spawn = spawnFn || ((cmd, args, opts) => getExecutor().spawn(cmd, args, opts));
|
|
1197
|
-
const outputFile = provider === "tailscale" ? null : logFile || (spawnFn ? null : createTunnelLogFile(provider));
|
|
1198
|
-
let child;
|
|
1179
|
+
function teardownOwnedAvd(avdName, options = {}) {
|
|
1180
|
+
let claim;
|
|
1199
1181
|
try {
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
"pipe",
|
|
1211
|
-
"pipe"
|
|
1212
|
-
],
|
|
1213
|
-
detached: true
|
|
1214
|
-
});
|
|
1215
|
-
} catch (err) {
|
|
1216
|
-
removeTunnelLogFile(outputFile);
|
|
1182
|
+
if (!isStimOwnedAvd(avdName)) return {
|
|
1183
|
+
status: "skipped",
|
|
1184
|
+
kind: "not-owned",
|
|
1185
|
+
reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
|
|
1186
|
+
};
|
|
1187
|
+
claim = acquireAvdClaim(avdName);
|
|
1188
|
+
const result = teardownClaimedAvd(avdName, claim, options);
|
|
1189
|
+
if (result.status === "torn-down" || result.status === "missing") options.onRemoved?.();
|
|
1190
|
+
return result;
|
|
1191
|
+
} catch (error) {
|
|
1217
1192
|
return {
|
|
1218
|
-
|
|
1219
|
-
reason:
|
|
1193
|
+
status: "failed",
|
|
1194
|
+
reason: String(error?.message || error)
|
|
1220
1195
|
};
|
|
1196
|
+
} finally {
|
|
1197
|
+
(0, ownership_claim_exports.releaseClaim)(claim);
|
|
1221
1198
|
}
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
if (
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1199
|
+
}
|
|
1200
|
+
function serialFree(serial, timeoutMs = AVD_INVENTORY_TIMEOUT_MS) {
|
|
1201
|
+
const adb = listAdbDevices({ timeoutMs });
|
|
1202
|
+
return ![
|
|
1203
|
+
...adb.emulators,
|
|
1204
|
+
...adb.physical,
|
|
1205
|
+
...adb.unhealthy
|
|
1206
|
+
].some((entry) => entry.serial === serial);
|
|
1207
|
+
}
|
|
1208
|
+
function teardownClaimedAvd(avdName, claim, { del = false, wipe = false, park, owner, workspace, orphanedDirectory, onlyIfMissing = false, onWipeStart, waitForShutdown = waitForAndroidEmulatorShutdown, assertStopped = assertOwnedAvdStopped, resolveAvd = (name) => resolveOwnedAvdSerial(name, { timeoutMs: AVD_INVENTORY_TIMEOUT_MS }) }) {
|
|
1209
|
+
let parkFallback;
|
|
1210
|
+
try {
|
|
1211
|
+
if (del || wipe) withConfigLock$1(() => assertAvdReferences(avdName, owner));
|
|
1212
|
+
const resolved = resolveAvd(avdName);
|
|
1213
|
+
if (resolved.notOwned) return {
|
|
1214
|
+
status: "skipped",
|
|
1215
|
+
kind: "not-owned",
|
|
1216
|
+
reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
|
|
1238
1217
|
};
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
failed: true,
|
|
1247
|
-
reason: `${reason} ${stopped.reason}`,
|
|
1248
|
-
cleanupFailed: true
|
|
1218
|
+
if (resolved.missing) {
|
|
1219
|
+
if (wipe) throw new Error(`AVD ${avdName} is not registered; its data was kept.`);
|
|
1220
|
+
return teardownUnregisteredAvd(avdName, del, owner, orphanedDirectory);
|
|
1221
|
+
}
|
|
1222
|
+
if (onlyIfMissing) return {
|
|
1223
|
+
status: "skipped",
|
|
1224
|
+
reason: "AVD registration appeared; its record was kept."
|
|
1249
1225
|
};
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
return
|
|
1226
|
+
if (orphanedDirectory) return {
|
|
1227
|
+
status: "skipped",
|
|
1228
|
+
reason: "AVD registration appeared; its data was kept."
|
|
1229
|
+
};
|
|
1230
|
+
const serial = resolved.serial;
|
|
1231
|
+
const stillStopped = (sessionSerial) => {
|
|
1232
|
+
const current = resolveAvd(avdName);
|
|
1233
|
+
return !current.notOwned && !current.missing && !current.serial && serialFree(sessionSerial);
|
|
1234
|
+
};
|
|
1235
|
+
if (serial) {
|
|
1236
|
+
const stillOwned = () => {
|
|
1237
|
+
const current = resolveAvd(avdName);
|
|
1238
|
+
return !current.notOwned && !current.missing && current.serial === serial;
|
|
1239
|
+
};
|
|
1240
|
+
closeOwnedDeviceSessions({
|
|
1241
|
+
platform: "android",
|
|
1242
|
+
id: serial,
|
|
1243
|
+
avdName
|
|
1244
|
+
}, stillOwned, workspace);
|
|
1245
|
+
if (!stillOwned()) throw new Error(`Owned AVD ${avdName} changed before shutdown; it was kept.`);
|
|
1246
|
+
waitForShutdown(avdName, (timeoutMs) => {
|
|
1247
|
+
(0, ownership_claim_exports.markClaimChildPending)(claim);
|
|
1248
|
+
try {
|
|
1249
|
+
shutdownAndroidEmulator(serial, timeoutMs);
|
|
1250
|
+
} finally {
|
|
1251
|
+
(0, ownership_claim_exports.clearClaimChild)(claim);
|
|
1252
|
+
}
|
|
1253
|
+
});
|
|
1254
|
+
const root = repoRootAbove(workspace);
|
|
1255
|
+
if (root) {
|
|
1256
|
+
const deadline = Date.now() + ADB_RELEASE_WAIT_MS;
|
|
1257
|
+
while (!serialFree(serial, Math.max(1, deadline - Date.now())) && Date.now() < deadline) sleepSync(250);
|
|
1258
|
+
closeOwnedDeviceSessions({
|
|
1259
|
+
platform: "android",
|
|
1260
|
+
id: serial,
|
|
1261
|
+
avdName
|
|
1262
|
+
}, stillStopped, workspace, root);
|
|
1263
|
+
}
|
|
1264
|
+
} else {
|
|
1265
|
+
closeOwnedDeviceSessions({
|
|
1266
|
+
platform: "android",
|
|
1267
|
+
id: null,
|
|
1268
|
+
avdName
|
|
1269
|
+
}, stillStopped, workspace);
|
|
1270
|
+
waitForShutdown(avdName, null);
|
|
1258
1271
|
}
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1272
|
+
if (wipe && !del) {
|
|
1273
|
+
const current = resolveAvd(avdName);
|
|
1274
|
+
if (current.notOwned || current.missing) throw new Error(`AVD ${avdName} changed before its data was wiped.`);
|
|
1275
|
+
if (current.serial) throw new Error(`Owned AVD ${avdName} started again before its data was wiped.`);
|
|
1276
|
+
assertStopped(avdName);
|
|
1277
|
+
withConfigLock$1(() => assertAvdReferences(avdName, owner));
|
|
1278
|
+
const directory = ownedAvdDirectory(avdName);
|
|
1279
|
+
if (!directory) throw new Error(`The data directory of AVD ${avdName} could not be resolved.`);
|
|
1280
|
+
onWipeStart?.();
|
|
1281
|
+
wipeAvdUserData(directory);
|
|
1282
|
+
}
|
|
1283
|
+
if (del) {
|
|
1284
|
+
const current = resolveAvd(avdName);
|
|
1285
|
+
if (current.notOwned) return {
|
|
1286
|
+
status: "skipped",
|
|
1287
|
+
kind: "not-owned",
|
|
1288
|
+
reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
|
|
1289
|
+
};
|
|
1290
|
+
if (current.missing) return teardownUnregisteredAvd(avdName, del, owner);
|
|
1291
|
+
if (current.serial) throw new Error(`Owned AVD ${avdName} started again before deletion.`);
|
|
1292
|
+
assertStopped(avdName);
|
|
1293
|
+
withConfigLock$1(() => assertAvdReferences(avdName, owner));
|
|
1294
|
+
if (park && park.max > 0) try {
|
|
1295
|
+
const systemImage = ownedAvdSystemImage(avdName);
|
|
1296
|
+
if (!systemImage || !park.configuration || !ownedAvdMatchesConfiguration(avdName, park.configuration)) throw new Error("the AVD has no verified creation configuration");
|
|
1297
|
+
const evicted = parkSim({
|
|
1298
|
+
platform: "android",
|
|
1299
|
+
projectPath: park.projectPath,
|
|
1300
|
+
slot: park.slot,
|
|
1301
|
+
max: park.max,
|
|
1302
|
+
record: {
|
|
1303
|
+
udid: avdName,
|
|
1304
|
+
name: avdName,
|
|
1305
|
+
systemImage,
|
|
1306
|
+
configuration: park.configuration,
|
|
1307
|
+
parkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1308
|
+
...park.appId ? { packageName: park.appId } : {},
|
|
1309
|
+
...park.cacheKey ? { cacheKey: park.cacheKey } : {}
|
|
1310
|
+
}
|
|
1311
|
+
});
|
|
1312
|
+
const removed = [];
|
|
1313
|
+
const failures = [];
|
|
1314
|
+
for (const entry of evicted) {
|
|
1315
|
+
const result = teardownParkedAvd(entry.name);
|
|
1316
|
+
if (result.status === "torn-down") removed.push({
|
|
1317
|
+
udid: entry.name,
|
|
1318
|
+
name: entry.name,
|
|
1319
|
+
platform: "android"
|
|
1320
|
+
});
|
|
1321
|
+
else if (result.status === "failed") failures.push(`could not delete evicted ${entry.name}: ${result.reason}`);
|
|
1322
|
+
}
|
|
1323
|
+
return {
|
|
1324
|
+
status: "torn-down",
|
|
1325
|
+
label: avdName,
|
|
1326
|
+
parked: {
|
|
1327
|
+
udid: avdName,
|
|
1328
|
+
name: avdName,
|
|
1329
|
+
platform: "android"
|
|
1330
|
+
},
|
|
1331
|
+
evicted: removed,
|
|
1332
|
+
...failures.length ? { evictionFailures: failures } : {}
|
|
1333
|
+
};
|
|
1334
|
+
} catch (error) {
|
|
1335
|
+
parkFallback = String(error?.message || error);
|
|
1336
|
+
}
|
|
1337
|
+
const directory = ownedAvdDirectory(avdName);
|
|
1338
|
+
const paths = avdStorageRoots().map((root) => join(root, `${avdName}.ini`));
|
|
1339
|
+
if (directory) paths.push(directory);
|
|
1340
|
+
(0, ownership_claim_exports.markClaimChildPending)(claim);
|
|
1341
|
+
try {
|
|
1342
|
+
deleteAvd(avdName);
|
|
1343
|
+
} finally {
|
|
1344
|
+
(0, ownership_claim_exports.clearClaimChild)(claim);
|
|
1345
|
+
}
|
|
1346
|
+
const remaining = paths.filter(avdPathExists);
|
|
1347
|
+
if (remaining.length) throw new Error(`AVD ${avdName} was only partially deleted; data remains at ${remaining.join(", ")}.`);
|
|
1348
|
+
}
|
|
1349
|
+
return {
|
|
1350
|
+
status: "torn-down",
|
|
1351
|
+
label: avdName,
|
|
1352
|
+
serial: resolved.serial ?? null,
|
|
1353
|
+
...parkFallback ? { parkFallback } : {}
|
|
1354
|
+
};
|
|
1355
|
+
} catch (e) {
|
|
1356
|
+
return {
|
|
1357
|
+
status: "failed",
|
|
1358
|
+
reason: String(e?.message || e)
|
|
1359
|
+
};
|
|
1273
1360
|
}
|
|
1274
|
-
const finalProcessToken = captureChildIdentity();
|
|
1275
|
-
if (!finalProcessToken || finalProcessToken !== initialProcessToken) return failAfterCleanup(`${provider} process identity changed before its tunnel could be recorded.`);
|
|
1276
|
-
if (!outputFile) unrefChildPipes(child);
|
|
1277
|
-
child.unref?.();
|
|
1278
|
-
if (childExited) return failAfterCleanup(`${provider} exited before its tunnel could be recorded.`, true);
|
|
1279
|
-
return {
|
|
1280
|
-
url,
|
|
1281
|
-
pid,
|
|
1282
|
-
processToken: initialProcessToken,
|
|
1283
|
-
logFile: outputFile,
|
|
1284
|
-
cleanup: () => cleanup()
|
|
1285
|
-
};
|
|
1286
1361
|
}
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1362
|
+
const BROWSER_EXIT_WAIT_MS = 15e3;
|
|
1363
|
+
const CHROME_KILL_WAIT_MS = 5e3;
|
|
1364
|
+
async function stopOwnedChrome(record, profile) {
|
|
1365
|
+
for (const signal of ["SIGTERM", "SIGKILL"]) {
|
|
1366
|
+
const state = chromeProcessState(record, profile);
|
|
1367
|
+
if (state === "gone") return true;
|
|
1368
|
+
if (state === "unknown") return false;
|
|
1369
|
+
try {
|
|
1370
|
+
signalProcessTree(record.pid, signal, { group: true });
|
|
1371
|
+
} catch {}
|
|
1372
|
+
const deadline = Date.now() + CHROME_KILL_WAIT_MS;
|
|
1373
|
+
while (chromeProcessState(record, profile) !== "gone" && Date.now() < deadline) await sleepAsync(50);
|
|
1374
|
+
}
|
|
1375
|
+
return chromeProcessState(record, profile) === "gone";
|
|
1376
|
+
}
|
|
1377
|
+
const sleepAsync = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
1378
|
+
/**
|
|
1379
|
+
* Closes the workspace's Stim-owned Chrome and its supervisor, after verifying both process identities. With
|
|
1380
|
+
* `deleteProfile`, it also deletes the profile directory, only when the created-devices ledger lists it.
|
|
1381
|
+
*/
|
|
1382
|
+
async function teardownOwnedBrowser(root, options = {}) {
|
|
1383
|
+
try {
|
|
1384
|
+
return await withWorkspaceProcessLock(workspaceDir(root), "web", () => teardownBrowserHeld(root, options), {
|
|
1385
|
+
external: true,
|
|
1386
|
+
ownerPurpose: "browser teardown"
|
|
1295
1387
|
});
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
failures.push(`${provider}: ${result.reason}`);
|
|
1301
|
-
if (result.cleanupFailed) return {
|
|
1302
|
-
failed: true,
|
|
1303
|
-
reason: failures.join(" ")
|
|
1388
|
+
} catch (error) {
|
|
1389
|
+
return {
|
|
1390
|
+
status: "failed",
|
|
1391
|
+
reason: String(error?.message || error)
|
|
1304
1392
|
};
|
|
1305
1393
|
}
|
|
1306
|
-
return {
|
|
1307
|
-
failed: true,
|
|
1308
|
-
reason: failures.join(" ") || "No managed tunnel provider was selected."
|
|
1309
|
-
};
|
|
1310
|
-
}
|
|
1311
|
-
function closeChildPipes(child) {
|
|
1312
|
-
child.stdout?.destroy?.();
|
|
1313
|
-
child.stderr?.destroy?.();
|
|
1314
|
-
}
|
|
1315
|
-
function unrefChildPipes(child) {
|
|
1316
|
-
resumeChildPipes(child);
|
|
1317
|
-
for (const stream of [child.stdout, child.stderr]) stream?.unref?.();
|
|
1318
1394
|
}
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
child.stderr?.resume?.();
|
|
1322
|
-
}
|
|
1323
|
-
async function signalAndWaitForExit(child, signal, { timeoutMs, now, sleep, isAlive, platform }) {
|
|
1324
|
-
let exited = false;
|
|
1325
|
-
const onExit = () => {
|
|
1326
|
-
exited = true;
|
|
1327
|
-
};
|
|
1328
|
-
child.once("exit", onExit);
|
|
1395
|
+
/** {@link teardownOwnedBrowser} for a caller that already holds the {@link BROWSER_LOCK} lock. */
|
|
1396
|
+
async function teardownBrowserHeld(root, { deleteProfile = false } = {}) {
|
|
1329
1397
|
try {
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1398
|
+
const record = readWebRecord(root);
|
|
1399
|
+
const profile = record?.profile ?? webProfileDir(root);
|
|
1400
|
+
if (record) {
|
|
1401
|
+
const supervisor = (0, process_identity_exports.inspectProcessIdentity)(record);
|
|
1402
|
+
const chrome = record.chromeProcess ? (0, process_identity_exports.inspectProcessIdentity)(record.chromeProcess) : "gone";
|
|
1403
|
+
if (supervisor === "unknown" || chrome === "unknown") return {
|
|
1404
|
+
status: "skipped",
|
|
1405
|
+
kind: "not-verified",
|
|
1406
|
+
reason: `the browser supervisor (pid ${record.pid}) or its Chrome could not be verified; its record was kept`
|
|
1407
|
+
};
|
|
1408
|
+
if (supervisor === "same") {
|
|
1409
|
+
try {
|
|
1410
|
+
process.kill(record.pid, "SIGTERM");
|
|
1411
|
+
} catch {}
|
|
1412
|
+
if (!await (0, process_identity_exports.waitForProcessExit)(record, BROWSER_EXIT_WAIT_MS)) return {
|
|
1413
|
+
status: "failed",
|
|
1414
|
+
reason: `browser supervisor pid ${record.pid} did not exit`
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
if (record.chromeProcess && !await stopOwnedChrome(record.chromeProcess, profile)) return {
|
|
1418
|
+
status: "failed",
|
|
1419
|
+
reason: `Chrome pid ${record.chromeProcess.pid} did not exit`
|
|
1420
|
+
};
|
|
1421
|
+
const exited = record.chromeProcess;
|
|
1422
|
+
const holder = liveProfileHolder(profile, { alive: (pid) => pidExists(pid) && !(pid === exited?.pid && (0, process_identity_exports.inspectProcessIdentity)(exited) === "gone") });
|
|
1423
|
+
if (holder !== null) return {
|
|
1424
|
+
status: "skipped",
|
|
1425
|
+
kind: "not-verified",
|
|
1426
|
+
reason: `pid ${holder} still holds ${profile}, and the browser record does not name it; it was left running`
|
|
1427
|
+
};
|
|
1428
|
+
const claims = (0, ownership_claim_exports.clearFreeClaimSet)({
|
|
1429
|
+
root: webClaimRoot(root),
|
|
1430
|
+
label: "browser supervisor"
|
|
1431
|
+
});
|
|
1432
|
+
if (claims.status !== "cleared") return {
|
|
1433
|
+
status: "skipped",
|
|
1434
|
+
kind: "not-verified",
|
|
1435
|
+
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))}`
|
|
1436
|
+
};
|
|
1437
|
+
clearWebRecord(root, record);
|
|
1438
|
+
await releaseBrowserPort(root, record.cdpPort);
|
|
1439
|
+
} else {
|
|
1440
|
+
const holder = liveProfileHolder(profile);
|
|
1441
|
+
if (holder !== null) return {
|
|
1442
|
+
status: "skipped",
|
|
1443
|
+
kind: "not-verified",
|
|
1444
|
+
reason: `pid ${holder} holds ${profile} but no browser supervisor is recorded; it was left running`
|
|
1445
|
+
};
|
|
1446
|
+
const port = getProject(root)?.ports?.[CDP_PORT_LABEL];
|
|
1447
|
+
if (typeof port === "number") await releaseBrowserPort(root, port);
|
|
1448
|
+
}
|
|
1449
|
+
if (existsSync(profile)) removeSingletonFiles(profile);
|
|
1450
|
+
if (deleteProfile && existsSync(profile)) {
|
|
1451
|
+
if (!readCreatedDevices$1().web.has(profile)) return {
|
|
1452
|
+
status: "skipped",
|
|
1453
|
+
kind: "not-owned",
|
|
1454
|
+
reason: `${profile} is not in Stim's ledger; it was kept`
|
|
1455
|
+
};
|
|
1456
|
+
rmSync(profile, {
|
|
1457
|
+
recursive: true,
|
|
1458
|
+
force: true
|
|
1459
|
+
});
|
|
1460
|
+
forgetCreatedDevice("web", profile);
|
|
1461
|
+
}
|
|
1462
|
+
return record ? {
|
|
1463
|
+
status: "torn-down",
|
|
1464
|
+
label: `Chrome (${profile})`
|
|
1465
|
+
} : { status: "missing" };
|
|
1466
|
+
} catch (error) {
|
|
1467
|
+
return {
|
|
1468
|
+
status: "failed",
|
|
1469
|
+
reason: String(error?.message || error)
|
|
1470
|
+
};
|
|
1335
1471
|
}
|
|
1336
|
-
const hasExited = () => exited;
|
|
1337
|
-
const deadline = now() + timeoutMs;
|
|
1338
|
-
while (!hasExited() && now() < deadline) await sleep(CLEANUP_POLL_MS);
|
|
1339
|
-
if (!hasExited() && child.pid && !isAlive(child.pid)) exited = true;
|
|
1340
|
-
child.removeListener("exit", onExit);
|
|
1341
|
-
return hasExited();
|
|
1342
1472
|
}
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
if (
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1473
|
+
//#endregion
|
|
1474
|
+
//#region src/macos/instances.ts
|
|
1475
|
+
const BUNDLE_EXECUTABLE = /^[^/]+\.app\/Contents\/MacOS\/[^/]+$/;
|
|
1476
|
+
function bundleDirs(root) {
|
|
1477
|
+
const dir = macosDir(root);
|
|
1478
|
+
if (process.platform !== "darwin" || !existsSync(dir)) return [];
|
|
1479
|
+
return [.../* @__PURE__ */ new Set([dir, realpathSync(dir)])];
|
|
1480
|
+
}
|
|
1481
|
+
function isBundleExecutable(path, dirs) {
|
|
1482
|
+
return dirs.some((dir) => path.startsWith(`${dir}/`) && BUNDLE_EXECUTABLE.test(path.slice(dir.length + 1)));
|
|
1483
|
+
}
|
|
1484
|
+
/** Pids from `ps -axww -o pid=,comm=` output whose command names a bundle executable inside `dirs`. */
|
|
1485
|
+
function bundleCandidates(psOutput, dirs) {
|
|
1486
|
+
const pids = [];
|
|
1487
|
+
for (const line of psOutput.split("\n")) {
|
|
1488
|
+
const match = /^\s*(\d+)\s+(.+)$/.exec(line);
|
|
1489
|
+
if (match && isBundleExecutable(match[2], dirs)) pids.push(Number(match[1]));
|
|
1357
1490
|
}
|
|
1358
|
-
|
|
1359
|
-
timeoutMs,
|
|
1360
|
-
now,
|
|
1361
|
-
sleep,
|
|
1362
|
-
isAlive,
|
|
1363
|
-
platform
|
|
1364
|
-
});
|
|
1365
|
-
closeChildPipes(child);
|
|
1366
|
-
return killed;
|
|
1491
|
+
return pids;
|
|
1367
1492
|
}
|
|
1368
|
-
|
|
1369
|
-
|
|
1493
|
+
/** Whether `lsof -Fn -d txt` output for one process names a bundle executable inside `dirs`. */
|
|
1494
|
+
function lsofNamesBundleExecutable(lsofOutput, dirs) {
|
|
1495
|
+
return lsofOutput.split("\n").some((line) => line.startsWith("n") && isBundleExecutable(line.slice(1), dirs));
|
|
1370
1496
|
}
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1497
|
+
function candidatePids(dirs) {
|
|
1498
|
+
if (!dirs.length) return [];
|
|
1499
|
+
return bundleCandidates(getExecutor().runFile("ps", [
|
|
1500
|
+
"-axww",
|
|
1501
|
+
"-o",
|
|
1502
|
+
"pid=,comm="
|
|
1503
|
+
], { timeoutMs: 1e4 }), dirs).filter((pid) => pid !== process.pid);
|
|
1375
1504
|
}
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
return { status: "missing" };
|
|
1380
|
-
};
|
|
1381
|
-
const pid = record?.pid;
|
|
1382
|
-
if (!pid || !isAlive(pid)) return missing();
|
|
1383
|
-
if (!record.processToken) return {
|
|
1384
|
-
status: "failed",
|
|
1385
|
-
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.`
|
|
1386
|
-
};
|
|
1387
|
-
const identity = inspectIdentity(record);
|
|
1388
|
-
if (identity === "gone" || identity === "different") return missing();
|
|
1389
|
-
if (identity !== "same") return {
|
|
1390
|
-
status: "failed",
|
|
1391
|
-
reason: `could not verify the process identity for tunnel pid ${pid}; refusing to signal it.`
|
|
1392
|
-
};
|
|
1393
|
-
try {
|
|
1394
|
-
kill(pid);
|
|
1395
|
-
} catch (err) {
|
|
1396
|
-
return isEsrch(err) ? missing() : {
|
|
1397
|
-
status: "failed",
|
|
1398
|
-
reason: describe(err)
|
|
1399
|
-
};
|
|
1400
|
-
}
|
|
1401
|
-
const deadline = now() + timeoutMs;
|
|
1402
|
-
const exited = () => {
|
|
1403
|
-
if (!isAlive(pid)) return true;
|
|
1404
|
-
const current = inspectIdentity(record);
|
|
1405
|
-
return current === "gone" || current === "different";
|
|
1406
|
-
};
|
|
1407
|
-
while (now() < deadline && !exited()) await sleep(STOP_POLL_MS);
|
|
1408
|
-
if (!exited()) return {
|
|
1409
|
-
status: "failed",
|
|
1410
|
-
reason: `pid ${pid} did not exit within ${timeoutMs}ms.`
|
|
1411
|
-
};
|
|
1412
|
-
removeRecordedTunnelLogFile(record.logFile);
|
|
1413
|
-
return { status: "stopped" };
|
|
1505
|
+
/** Pids that claim to run an app bundle from the workspace's macOS directory. `ps` reports argv[0], so this is a lead. */
|
|
1506
|
+
function bundleInstancePids(root) {
|
|
1507
|
+
return candidatePids(bundleDirs(root));
|
|
1414
1508
|
}
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
function resolveSupervisorTarget({ state, record, reservedPort, isAlive = pidExists, inspectIdentity = process_identity_exports.inspectProcessIdentity } = {}) {
|
|
1418
|
-
const statePid = numberOrNull(state?.pid);
|
|
1419
|
-
const recordPid = numberOrNull(record?.pid);
|
|
1420
|
-
const pid = statePid ?? recordPid;
|
|
1421
|
-
if (!pid) return { status: "none" };
|
|
1422
|
-
if (statePid && recordPid && statePid !== recordPid) return {
|
|
1423
|
-
status: "unverified",
|
|
1424
|
-
pid,
|
|
1425
|
-
reason: `workspace state.json records supervisor pid ${statePid} but the registry records pid ${recordPid}`
|
|
1426
|
-
};
|
|
1427
|
-
if (!isAlive(pid)) return {
|
|
1428
|
-
status: "stale",
|
|
1429
|
-
pid
|
|
1430
|
-
};
|
|
1431
|
-
const port = numberOrNull(state?.port) ?? numberOrNull(record?.port);
|
|
1432
|
-
if (reservedPort !== null && reservedPort !== void 0 && port !== null && port !== reservedPort) return {
|
|
1433
|
-
status: "unverified",
|
|
1434
|
-
pid,
|
|
1435
|
-
reason: `supervisor pid ${pid} records port ${port}, but this project reserved port ${reservedPort}`
|
|
1436
|
-
};
|
|
1437
|
-
if (reservedPort !== null && reservedPort !== void 0 && port === null) return {
|
|
1438
|
-
status: "unverified",
|
|
1439
|
-
pid,
|
|
1440
|
-
reason: `supervisor pid ${pid} has no recorded port, so it cannot be matched against reserved port ${reservedPort}`
|
|
1441
|
-
};
|
|
1442
|
-
const identityRecord = statePid ? state : record;
|
|
1443
|
-
if (statePid && recordPid && state?.processToken !== record?.processToken) return {
|
|
1444
|
-
status: "unverified",
|
|
1445
|
-
pid,
|
|
1446
|
-
reason: "workspace state and registry process identities disagree"
|
|
1447
|
-
};
|
|
1448
|
-
const identity = inspectIdentity(identityRecord);
|
|
1449
|
-
if (identity === "gone" || identity === "different") return {
|
|
1450
|
-
status: "stale",
|
|
1451
|
-
pid
|
|
1452
|
-
};
|
|
1453
|
-
if (identity !== "same") return {
|
|
1454
|
-
status: "unverified",
|
|
1455
|
-
pid,
|
|
1456
|
-
reason: "the recorded process identity could not be verified"
|
|
1457
|
-
};
|
|
1458
|
-
return {
|
|
1459
|
-
status: "ours",
|
|
1460
|
-
processToken: identityRecord.processToken,
|
|
1461
|
-
pid,
|
|
1462
|
-
port,
|
|
1463
|
-
mode: state?.mode ?? record?.mode ?? null,
|
|
1464
|
-
startedAt: state?.startedAt ?? record?.startedAt ?? null
|
|
1465
|
-
};
|
|
1509
|
+
function unverified(pid, why) {
|
|
1510
|
+
return Object.assign(/* @__PURE__ */ new Error(`Cannot verify macOS app pid ${pid}: ${why}; no signal was sent.`), { code: "STIM_MACOS_OWNER_UNVERIFIED" });
|
|
1466
1511
|
}
|
|
1467
|
-
|
|
1468
|
-
|
|
1512
|
+
/**
|
|
1513
|
+
* Every running process whose executable is an app bundle Stim built in the workspace's macOS directory, however it
|
|
1514
|
+
* was launched. Each identity is captured before its executable is confirmed with lsof.
|
|
1515
|
+
*/
|
|
1516
|
+
function runningBundleInstances(root) {
|
|
1517
|
+
const dirs = bundleDirs(root);
|
|
1518
|
+
const instances = [];
|
|
1519
|
+
for (const pid of candidatePids(dirs)) {
|
|
1520
|
+
const processToken = (0, process_identity_exports.captureProcessToken)(pid);
|
|
1521
|
+
const start = (0, process_identity_exports.processStartMicros)(pid);
|
|
1522
|
+
if (!processToken || start.status !== "running") {
|
|
1523
|
+
if (start.status === "gone") continue;
|
|
1524
|
+
throw unverified(pid, "its identity cannot be captured");
|
|
1525
|
+
}
|
|
1526
|
+
const record = {
|
|
1527
|
+
pid,
|
|
1528
|
+
processToken,
|
|
1529
|
+
startedAtMicros: start.startedAtMicros
|
|
1530
|
+
};
|
|
1531
|
+
let open;
|
|
1532
|
+
try {
|
|
1533
|
+
open = getExecutor().runFile("lsof", [
|
|
1534
|
+
"-a",
|
|
1535
|
+
"-p",
|
|
1536
|
+
String(pid),
|
|
1537
|
+
"-d",
|
|
1538
|
+
"txt",
|
|
1539
|
+
"-Fn"
|
|
1540
|
+
], { timeoutMs: 1e4 });
|
|
1541
|
+
} catch (error) {
|
|
1542
|
+
if (["gone", "different"].includes((0, process_identity_exports.inspectProcessIdentity)(record))) continue;
|
|
1543
|
+
open = String(error.stdout ?? "");
|
|
1544
|
+
if (!open.trim()) throw unverified(pid, `lsof failed (${error.message.split("\n")[0]})`);
|
|
1545
|
+
}
|
|
1546
|
+
if (lsofNamesBundleExecutable(open, dirs)) instances.push(record);
|
|
1547
|
+
}
|
|
1548
|
+
return instances;
|
|
1469
1549
|
}
|
|
1470
1550
|
//#endregion
|
|
1471
1551
|
//#region src/workspace/in-use.ts
|
|
@@ -1486,6 +1566,12 @@ function workspaceInUse(root, { supervisor = true, managedLocks = true, hosted =
|
|
|
1486
1566
|
if (macos?.host) reasons.push(`its macOS app runs on ${macos.host.machine}`);
|
|
1487
1567
|
else if (macos?.state === "running" || macos?.state === "orphaned" || macos?.build.state === "running") reasons.push("its macOS app or build is running");
|
|
1488
1568
|
else if (macos?.state === "unverified" || !macos && readWorkspaceState$1(root)?.macos !== void 0) reasons.push("its macOS process owner cannot be verified");
|
|
1569
|
+
else try {
|
|
1570
|
+
const pids = bundleInstancePids(root);
|
|
1571
|
+
if (pids.length) reasons.push(`its macOS app is running (pid ${pids.join(", ")})`);
|
|
1572
|
+
} catch (error) {
|
|
1573
|
+
reasons.push(`its running macOS apps cannot be listed: ${error.message}`);
|
|
1574
|
+
}
|
|
1489
1575
|
const runtime = (0, ownership_claim_exports.readClaimSet)(macosRuntimeClaim(root));
|
|
1490
1576
|
if (runtime.live.length) reasons.push("its macOS runtime claim is held");
|
|
1491
1577
|
else if (runtime.unresolved[0]) reasons.push(`its macOS runtime cannot be verified: ${runtime.unresolved[0].reason}`);
|
|
@@ -1855,4 +1941,4 @@ function sizeText(bytes) {
|
|
|
1855
1941
|
return bytes === null ? "size unknown" : formatBytes(bytes);
|
|
1856
1942
|
}
|
|
1857
1943
|
//#endregion
|
|
1858
|
-
export {
|
|
1944
|
+
export { withManagedRemoteWorktreeRemovalLock as A, recordGcResult as C, stopTunnel as D, startTunnelSequence as E, acquireBuildSlot as F, listBuildSlots as I, releaseBuildSlot as L, withManagedTunnelRemovalLock as M, probePublicHttp as N, terminateChild as O, resolveSupervisorTarget as P, teardownParkedIosSim as S, canonicalPath as T, teardownBrowserHeld as _, collectWorkspaceOutputs as a, teardownOwnedIosSim as b, listWorkspaceDirs as c, emptyWorkspaceDir as d, withIdleWorkspace as f, eraseParkedIosSim as g, eraseParkedAvd as h, collectOrphanedWorkspaces as i, withManagedTunnelLock as j, withManagedRemoteWorktreeLock as k, rootPresence as l, runningBundleInstances as m, WORKSPACE_OUTPUT_DIRS as n, deleteOrphanedWorkspaces as o, workspaceInUse as p, clearWorkspaceOutputs as r, isInsideWorkspaces as s, REBUILD_COST as t, sizeText as u, teardownOwnedAvd as v, takeGcResults as w, teardownParkedAvd as x, teardownOwnedBrowser as y };
|