stim 1.20.2 → 1.21.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 +88 -8
- package/dist/acquire-artifact-CD4lbiWE.mjs +153 -0
- package/dist/act-DHAw2A2S.mjs +2667 -0
- package/dist/activity-BzVfHSrY.mjs +1376 -0
- package/dist/{agent-device-usage-output-Dzgv6lgR.mjs → agent-device-usage-output-zh1vhbLp.mjs} +21 -65
- package/dist/{android-HqP1_vB_.mjs → android-BEJ8Jpl7.mjs} +2 -2
- package/dist/{android-DCKZrNvV.mjs → android-BLgbNqox.mjs} +128 -78
- package/dist/{android-Dlzxshgp.mjs → android-CV_GCELZ.mjs} +5 -5
- package/dist/{android-Mz_Inn3w.mjs → android-DUYkNF4u.mjs} +184 -102
- package/dist/{android-cas-Bl-cLJBk.mjs → android-cas-Ct5b1xCK.mjs} +7 -6
- package/dist/android-cas-compiler.mjs +2 -2
- package/dist/android-project-BCAAmMwz.mjs +12 -0
- package/dist/api.d.mts +740 -8
- package/dist/api.mjs +585 -1
- package/dist/{artifact-HSwkIWEQ.mjs → artifact-B8T3S5gi.mjs} +223 -201
- package/dist/{artifact-D3czlUvT.mjs → artifact-DAuZ-FNU.mjs} +158 -148
- package/dist/{browser-web-CA-F1GQo.mjs → browser-web-QQzVjz3O.mjs} +26 -24
- package/dist/budget-Bs6SxxzO.mjs +9154 -0
- package/dist/{build-DYDbfkk5.mjs → build-CoPzQRNw.mjs} +144 -96
- package/dist/build-cache-DmyR9Zzc.mjs +602 -0
- package/dist/build-plan-CaFskB7_.mjs +188 -0
- package/dist/{cache-manifest-XH-fvDUj.mjs → cache-manifest-DkkqguoK.mjs} +2 -2
- package/dist/cache-manifest.mjs +1 -1
- package/dist/{ccache-zdKiYQ3E.mjs → ccache-BJRZ9pG3.mjs} +10 -9
- package/dist/{cdp-Dq_ypAlJ.mjs → cdp-CWmIUMbw.mjs} +18 -5
- package/dist/{chrome-BnzZMqsR.mjs → chrome-B6tfCta1.mjs} +7 -4
- package/dist/{cli-CLP3diBm.mjs → cli-0DYf8QhQ.mjs} +20 -20
- package/dist/cli.mjs +1 -1
- package/dist/client-B7QVOlCr.d.mts +253 -0
- package/dist/client-UXD70ZbM.mjs +1406 -0
- package/dist/collector-run.d.mts +4 -40
- package/dist/collector-run.mjs +9 -8
- package/dist/{command-output-BTy8hZ0F.mjs → command-output-B1UrjZHL.mjs} +1 -1
- package/dist/config-CYcBF478.mjs +949 -0
- package/dist/{created-devices-Dzd5C2LF.mjs → created-devices-CqeBlrEn.mjs} +3 -3
- package/dist/{dependency-state-DG0p31-y.mjs → dependency-state-BbKQW83h.mjs} +20 -5
- package/dist/{detached-entry-B9C_zeQC.mjs → detached-entry-DFRyG1_o.mjs} +1 -1
- package/dist/{dev-client-DAWVIGBu.mjs → dev-client-UOeESKm6.mjs} +2 -2
- package/dist/device-DCxH7_By.mjs +1068 -0
- package/dist/{device-BECOiufl.mjs → device-ExJq8Ei7.mjs} +35 -35
- package/dist/{device-capacity-CBUAcRyQ.mjs → device-capacity-CMlqEcvw.mjs} +43 -13
- package/dist/device-host-worker.mjs +65 -61
- package/dist/{device-ios-COSi6oS1.mjs → device-ios-DRUeKaFV.mjs} +48 -45
- package/dist/{device-lease-run-Cw2M4z51.mjs → device-lease-run-C8raGFoP.mjs} +14 -26
- package/dist/{device-pool-Bf-pOhSw.mjs → device-pool-DMRpVX9a.mjs} +5 -5
- package/dist/doctor-BLHCYxzi.mjs +2 -0
- package/dist/{doctor-kfMBnUw6.mjs → doctor-DTvaDOda.mjs} +281 -317
- package/dist/{doctor-CB8oZKCk.mjs → doctor-Dhqebm0q.mjs} +183 -91
- package/dist/doctor-fingerprint.mjs +19 -0
- package/dist/{doctor-watchman-BRyV2adA.mjs → doctor-watchman-Cv1F10Kj.mjs} +40 -21
- package/dist/{eas-build-JyfqyHfC.mjs → eas-build-BomfRnFT.mjs} +53 -20
- package/dist/{build-plan-B7SZsc2N.mjs → eas-fallback-DJwZpy0c.mjs} +26 -208
- package/dist/{watchman-CPQFZLXY.mjs → eas-session-ledger-op6A54yi.mjs} +229 -11
- package/dist/{error-diagnostics-BR677IDM.mjs → error-diagnostics-Bf0rZvG9.mjs} +172 -82
- package/dist/exec-H20xSg2T.d.mts +65 -0
- package/dist/exec-_UbtCQcG.mjs +750 -0
- package/dist/gc-C-WwL0yi.mjs +3 -0
- package/dist/{gc-qWOMdnER.mjs → gc-_-dCXdyL.mjs} +584 -796
- package/dist/git-C3iTsJaZ.mjs +73 -0
- package/dist/{guide-G9cAbVAc.mjs → guide-Yqwt2bd6.mjs} +941 -269
- package/dist/{hosted-android-C3MHGld1.mjs → hosted-android-Bw-honsd.mjs} +8 -6
- package/dist/{hosted-client-ZEKpS9vw.mjs → hosted-client-DB_gBPfY.mjs} +34 -19
- package/dist/{hosted-ios-l-o3iM9J.mjs → hosted-ios-CUvOf4Vr.mjs} +1 -1
- package/dist/{hosted-logs-EzQM7-3Z.mjs → hosted-logs-DL_P5AdN.mjs} +77 -43
- package/dist/{hosted-macos-BACVAVqE.mjs → hosted-macos-QI141yKk.mjs} +21 -17
- package/dist/{hosted-native-A4s1QPXe.mjs → hosted-native-Ksyjgzq-.mjs} +30 -21
- package/dist/idle-BVeRGroT.d.mts +24 -0
- package/dist/{idle-shutdown-DtxhPTKe.mjs → idle-shutdown-DaKmZi0z.mjs} +11 -10
- package/dist/{ios-BwVCF7oA.mjs → ios-CQ68hG9s.mjs} +8 -8
- package/dist/{ios-RkW4pnQf.mjs → ios-CpByZy12.mjs} +140 -80
- package/dist/{ios-device-B5veSDDQ.mjs → ios-device-CXGP1bsD.mjs} +1 -1
- package/dist/{ios-device-D7pi2Sca.mjs → ios-device-CwFMehUS.mjs} +51 -26
- package/dist/{ios-project-K9hY_Jab.mjs → ios-project-WuwwBdvW.mjs} +13 -13
- package/dist/{ios-nOLJGVdy.mjs → ios-qNJt4dFm.mjs} +1 -1
- package/dist/ios-signing-DTTIChGs.mjs +489 -0
- package/dist/{ios-state-CBLe1xYw.mjs → ios-state-DvFX-K_l.mjs} +1 -1
- package/dist/{ios-Ce7bc7br.mjs → ios-ubFsBzwq.mjs} +129 -86
- package/dist/{js-swap-Bb3nsS07.mjs → js-swap-Cc1dttps.mjs} +53 -26
- package/dist/{launch-F4sOqAhB.mjs → launch-CD1yVc7U.mjs} +91 -90
- package/dist/{launch-C3qJ6Agb.mjs → launch-Zq2mOSDy.mjs} +82 -541
- package/dist/logs-BS00Anxf.mjs +352 -0
- package/dist/logs-CMm05HxN.mjs +255 -0
- package/dist/{macos-D7uDvO-z.mjs → macos-B65--pPd.mjs} +1 -1
- package/dist/{macos-Ch5VJmcS.mjs → macos-C-0qJ6zq.mjs} +225 -212
- package/dist/macos-run.mjs +1 -1
- package/dist/maintenance-run.mjs +122 -247
- package/dist/{metro-DnCEjCxs.mjs → metro-DEXpIhHC.mjs} +67 -12
- package/dist/{metro-gateway-CX9Xlkq2.mjs → metro-gateway-BMJNlj3E.mjs} +20 -12
- package/dist/{named-ports-4WopRPg-.mjs → named-ports-BWTzt6mu.mjs} +8 -63
- package/dist/native-android-JACyjTZs.mjs +844 -0
- package/dist/{native-gradle-inputs-BsLRrl7u.mjs → native-gradle-inputs-Bep9JcSF.mjs} +128 -140
- package/dist/native-inputs-TRTTTRxt.mjs +91 -0
- package/dist/native-run-DY_RLx3C.mjs +277 -0
- package/dist/{native-runtime-k8dTIrbK.mjs → native-runtime-Dg1Q7tfF.mjs} +9 -8
- package/dist/{native-xcode-inputs-CykopJdM.mjs → native-xcode-inputs-CB2iQeWq.mjs} +27 -18
- package/dist/{native-xcode-ios-Bbf30Gm0.mjs → native-xcode-ios-CXX39F7o.mjs} +96 -47
- package/dist/{ndjson-GOnbyvso.mjs → ndjson-CgE_o3Dl.mjs} +3 -3
- package/dist/offload-worker.d.mts +1 -46
- package/dist/offload-worker.mjs +197 -66
- package/dist/{ownership-DCtw_2IA.mjs → ownership-DEmM5vhh.mjs} +3 -3
- package/dist/{page-DRHy329N.mjs → page-CPaHeYqj.mjs} +7 -4
- package/dist/paths-BsJMoPP2.mjs +12 -0
- package/dist/paths-DQtTgsBf.mjs +42 -0
- package/dist/{placement-log-MOfPVgaM.mjs → placement-log-BqGbTa61.mjs} +5 -5
- package/dist/{plan-placement-V0TdzYXp.mjs → plan-placement-Bwj-IjY1.mjs} +6 -6
- package/dist/{ports-DdSUicVo.mjs → ports-D-MmCTdv.mjs} +3 -3
- package/dist/prebuild-uD_ipCxF.mjs +449 -0
- package/dist/{project-_phNIJfs.mjs → project-BZNjmW_8.mjs} +65 -37
- package/dist/{pull-request-CQ8sPbZa.mjs → pull-request-Lfetn8Kr.mjs} +2 -2
- package/dist/pull-request-zjCSAryH.d.mts +120 -0
- package/dist/pull-requests.d.mts +1 -119
- package/dist/pull-requests.mjs +1 -1
- package/dist/{react-native-android-CM1XRDwv.mjs → react-native-android-DuzH0cn3.mjs} +74 -48
- package/dist/{gradle-DSrXb6lG.mjs → react-native-build-rhk4smwQ.mjs} +292 -15
- package/dist/react-native-doctor-CQpuIMso.mjs +234 -0
- package/dist/{react-native-ios-Dhq_eiYQ.mjs → react-native-ios-Bhq1rJeV.mjs} +46 -28
- package/dist/{recordings-D0d5aQt0.mjs → recordings-DYteQSu4.mjs} +12 -9
- package/dist/{reload-C8JhxjSh.mjs → reload-CmTRFJaX.mjs} +93 -47
- package/dist/reload-DzjSl5P_.mjs +2 -0
- package/dist/{remote-BiR6aYjy.mjs → remote-ByYkXdGD.mjs} +15 -10
- package/dist/result-DNiS-tTP.mjs +434 -0
- package/dist/{run-BtPgpKks.mjs → run-D23HmJMs.mjs} +4 -5
- package/dist/selection-CSl9i5qU.mjs +619 -0
- package/dist/{server-bare-R2m_gsH8.mjs → server-bare-CxTrvDg8.mjs} +3 -4
- package/dist/{server-command-CTtCkmcQ.mjs → server-command-CtzloH3v.mjs} +14 -7
- package/dist/server-expo-C-90EhcG.mjs +2 -0
- package/dist/{server-expo-DsguMGUF.mjs → server-expo-DT_F_uVI.mjs} +50 -9
- package/dist/{settings-B717odTD.mjs → settings-CsJDEXgK.mjs} +234 -24
- package/dist/{settings-vk3mJGSm.d.mts → settings-DkdmlelE.d.mts} +6 -4
- package/dist/settings-T1gtc3bf.mjs +114 -0
- package/dist/settings-write-HyVZ8eQ8.mjs +359 -0
- package/dist/settings.schema.json +163 -16
- package/dist/{simslim-CCr3rE-A.mjs → simslim-Cgk17SXV.mjs} +14 -12
- package/dist/{slot-launch-DPt0m9Nd.mjs → slot-launch-DF8VfT64.mjs} +15 -20
- package/dist/{spawn-entry-Dcb7sDUz.mjs → spawn-entry-jw19m8Nl.mjs} +0 -1
- package/dist/stage-BIS0ydsV.mjs +183 -0
- package/dist/{start-BWqzODpb.mjs → start-7lpH7hmN.mjs} +63 -44
- package/dist/{start-TXeMmmO2.mjs → start-RChynRDp.mjs} +1 -1
- package/dist/{state-D1Icyjlu.mjs → state-C4ZtUisW.mjs} +40 -13
- package/dist/{state-CG_OFoaA.mjs → state-XMziexv2.mjs} +3 -2
- package/dist/{stats-BoY5sEPU.mjs → stats-53T7OTb9.mjs} +7 -13
- package/dist/stats-report-BFfMpWWF.mjs +16 -0
- package/dist/{status-WgFbkRzj.mjs → status-BL7jVyAB.mjs} +535 -447
- package/dist/status-CSi6tlRq.mjs +2 -0
- package/dist/{status-BTA7kKiQ.mjs → status-DqEUSSxs.mjs} +6 -4
- package/dist/{status-watch-C23ILtf9.mjs → status-watch-GDCfqpTE.mjs} +187 -22
- package/dist/status-worker.d.mts +1 -0
- package/dist/status-worker.mjs +19 -0
- package/dist/stim-desktop-C12we7lz.mjs +112 -0
- package/dist/{stop-DR6VoDIk.mjs → stop-Bk_1qiV6.mjs} +2 -2
- package/dist/{stop-BNmBMi1F.mjs → stop-CPzlM1L-.mjs} +138 -98
- package/dist/{stop-DPEhunlw.mjs → stop-D7h6jAXQ.mjs} +11 -10
- package/dist/supervisor-run.d.mts +8 -17
- package/dist/supervisor-run.mjs +63 -27
- package/dist/{support-DMTqj3Hm.mjs → support-67vraa1Q.mjs} +18 -11
- package/dist/{support-CwjbFs9H.mjs → support-Dlu37g6H.mjs} +12 -12
- package/dist/{swiftpm-macos-DRNVJWlb.mjs → swiftpm-macos-p8WZbxKU.mjs} +35 -24
- package/dist/toolchain-DhKwp9iI.mjs +2 -0
- package/dist/toolchain-Hp9sCpK4.mjs +873 -0
- package/dist/{trigger-CigAYjeL.mjs → trigger-BAahxUPq.mjs} +20 -5
- package/dist/trigger-Bf3uDcpv.mjs +2 -0
- package/dist/{warm-progress-BuY-Fvfn.mjs → warm-progress-BHX5r0Ei.mjs} +250 -156
- package/dist/{web-BDBMxDpx.mjs → web-CJmIj6AI.mjs} +18 -13
- package/dist/web-DknQETPs.mjs +2 -0
- package/dist/web-run.mjs +11 -12
- package/dist/windows-report.d.mts +1 -0
- package/dist/windows-report.mjs +18 -0
- package/dist/windows-report.ps1 +43 -0
- package/dist/workspace-process-lock-1_1OoVW5.mjs +57 -0
- package/dist/{workspace-state-DJt_MMvf.mjs → workspace-state-Cjuigeeu.mjs} +62 -58
- package/dist/{workspaces-C36s2jb9.mjs → workspaces-BccMFmGQ.mjs} +1188 -1079
- package/dist/worktree-CK2MpJW5.mjs +2 -0
- package/dist/{worktree-CgvJYJ-Q.mjs → worktree-DQ6Zi2Mv.mjs} +304 -53
- package/dist/xcode-Zlx3qjEs.mjs +896 -0
- package/package.json +7 -5
- package/shim/android-optimizations.gradle +7 -5
- package/dist/activity-BSV745Kz.mjs +0 -629
- package/dist/api-qvOzWyfO.mjs +0 -154
- package/dist/api-run.mjs +0 -294
- package/dist/app-install-BIKtMfPq.mjs +0 -896
- package/dist/artifact-lifecycle-DzPpBYvK.mjs +0 -30
- package/dist/budget-f7516Zk0.mjs +0 -4106
- package/dist/build-facts-DcLv2HSd.d.mts +0 -111
- package/dist/cancellation-C_22VQTH.mjs +0 -24
- package/dist/client-qQAaXSUs.mjs +0 -1771
- package/dist/config-BYDxlMY3.mjs +0 -562
- package/dist/device-DuiPJQSn.mjs +0 -290
- package/dist/device-lease-BX1sJQ2E.mjs +0 -1224
- package/dist/device-remote-xRT0Vmo9.mjs +0 -1272
- package/dist/eas-session-ledger-Cm0Gwq2F.mjs +0 -207
- package/dist/exec-DqKo9GKJ.mjs +0 -268
- package/dist/git-DDfFzkMm.mjs +0 -45
- package/dist/launch-verify-B6Q8hZP-.mjs +0 -428
- package/dist/logs-BGrEUaEs.mjs +0 -383
- package/dist/miss-reason-mKzZ4VvN.mjs +0 -238
- package/dist/native-android-D5qc3-_v.mjs +0 -226
- package/dist/ndjson-Du3j0AQ9.d.mts +0 -17
- package/dist/plan-C4TjdPsR.mjs +0 -260
- package/dist/prebuild-DYbucPXy.mjs +0 -212
- package/dist/preview-78a9kR_-.mjs +0 -1011
- package/dist/react-native-build-CtCccJtb.mjs +0 -239
- package/dist/react-native-doctor-CfRnFfjs.mjs +0 -78
- package/dist/remote-cache-OyOyVIS3.mjs +0 -526
- package/dist/result-CaJg5Iat.mjs +0 -186
- package/dist/server-expo-Bigriskl.mjs +0 -2
- package/dist/settings-BORNaaCO.mjs +0 -402
- package/dist/spawn-claims-DVS5RkMS.mjs +0 -72
- package/dist/stage-KNTQopqv.mjs +0 -157
- package/dist/stim-desktop-BzBN2Em2.mjs +0 -63
- package/dist/tailnet-Dh-qiYzG.mjs +0 -265
- package/dist/toolchain-BFkAcstd.mjs +0 -2
- package/dist/toolchain-DxSsNssu.mjs +0 -1657
- package/dist/trigger-9_USgmxz.mjs +0 -2
- package/dist/web-Dq-i3wzJ.mjs +0 -2
- package/dist/worktree-DpLdCHga.mjs +0 -2
- package/dist/{api-run.d.mts → doctor-fingerprint.d.mts} +0 -0
- package/dist/{errors-DzkMYh8Q.mjs → errors-ccaPc4ox.mjs} +2 -2
|
@@ -1,38 +1,60 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { h as listBuildLocks } from "./device-lease-BX1sJQ2E.mjs";
|
|
5
|
-
import { T as workspaceName, r as readWorkspaceState$1, s as workspaceLastUsed, x as workspaceDir, y as workspaceAgentDeviceDir } from "./workspace-state-DJt_MMvf.mjs";
|
|
1
|
+
import { c as stopDeclaringSpawnsOn, f as ownership_claim_exports, i as declareSpawnsOn, p as process_identity_exports, t as getExecutor } from "./exec-_UbtCQcG.mjs";
|
|
2
|
+
import { d as workspaceDir, h as workspaceName$1, l as workspaceAgentDeviceDir } from "./paths-DQtTgsBf.mjs";
|
|
3
|
+
import { D as withConfigLock$1, F as isOnMountedVolume, I as listMountedVolumes, K as projectDeviceSlots, O as withConfigLockAsync$1, P as formatBytes, R as measuredDirectorySizeAsync, U as deviceSlotPlatforms, _ as loadConfig$1, d as getConfigDir, g as isPathPrefix, j as acquireAvdClaim, p as getProject } from "./config-CYcBF478.mjs";
|
|
6
4
|
import { t as createLineReader } from "./process-output-Fpl8PYi8.mjs";
|
|
7
|
-
import { a as formatElapsed, l as phaseLine } from "./command-output-
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import { buildSlotsDir, hostedNativeRecords, isJsonObject, macosAppState, parseHostedNativePlacement, readJsonFile, readMacosRecord, requireAutomaticMachine, tryAcquireBuildSlotClaim, unreadableHostedNative } from "@stim-cli/core/state";
|
|
21
|
-
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";
|
|
5
|
+
import { a as formatElapsed, l as phaseLine } from "./command-output-B1UrjZHL.mjs";
|
|
6
|
+
import { l as withWorkspaceStateLockAsync, r as readWorkspaceState$1, u as workspaceLastUsed } from "./workspace-state-Cjuigeeu.mjs";
|
|
7
|
+
import { It as repoRootAsync, Qt as cancellableSleep, en as throwIfCancelled, ut as resolveTailscaleBinary } from "./settings-CsJDEXgK.mjs";
|
|
8
|
+
import { a as macosRuntimeClaim, n as macosDir } from "./state-XMziexv2.mjs";
|
|
9
|
+
import { l as pidExists, m as signalProcessTreeAsync, p as signalProcessTree } from "./metro-DEXpIhHC.mjs";
|
|
10
|
+
import { _ as chromeProcessStateAsync, c as webClaimRoot, i as clearWebRecord, o as readWebRecord, p as webProfileDir, t as CDP_PORT_LABEL, u as webFacts, v as liveProfileHolder, y as removeSingletonFiles } from "./state-C4ZtUisW.mjs";
|
|
11
|
+
import { t as canonicalPath } from "./paths-BsJMoPP2.mjs";
|
|
12
|
+
import { N as listBuildLocks, d as readAgentDeviceRecords, t as agentDeviceDaemon } from "./activity-BzVfHSrY.mjs";
|
|
13
|
+
import { n as readCreatedDevices$1, r as recordCreatedDevice, t as forgetCreatedDevice } from "./created-devices-CqeBlrEn.mjs";
|
|
14
|
+
import { A as listAdbDevicesAsync, B as ownedAvdMatchesConfiguration, F as listOrphanedAvdDirectories, Q as resolveOwnedAvdSerialAsync, W as ownedAvdSystemImage, _ as avdStorageRoots, _t as removeParkedAfter, at as wipeAvdUserData, et as shutdownAndroidEmulator, f as assertOwnedAvdStopped, ft as eraseParkedAfter, gt as readParked, h as avdPathExists, mt as parkSim, rt as waitForAndroidEmulatorShutdown, st as isStimOwnedAvd, x as deleteAvd, z as ownedAvdDirectory } from "./android-DUYkNF4u.mjs";
|
|
15
|
+
import { E as resolveOwnedIosSim, O as shutdownIosSim, S as renameIosSim, c as eraseIosSim, f as listAllIosSimsAsync, o as deleteIosSim, p as listIosDeviceTypesAsync, s as deleteParkedIosSim, v as parkedSimName, y as parseRuntimeVersion } from "./ios-CpByZy12.mjs";
|
|
16
|
+
import { n as workspaceProcessLockError, r as workspaceProcessLockPath, t as withWorkspaceProcessLock } from "./workspace-process-lock-1_1OoVW5.mjs";
|
|
17
|
+
import { r as releaseBrowserPort } from "./named-ports-BWTzt6mu.mjs";
|
|
22
18
|
import { createHash, randomUUID } from "node:crypto";
|
|
19
|
+
import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, watch, writeFileSync } from "node:fs";
|
|
20
|
+
import { isDeepStrictEqual } from "node:util";
|
|
23
21
|
import { homedir } from "node:os";
|
|
22
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
23
|
+
import { ambientContext, ambientEnv, ambientWrite } from "@stim-cli/core";
|
|
24
|
+
import { buildSlotsDir, hostedNativeRecords, isJsonObject, macosAppState, parseHostedNativePlacement, readJsonFile, readMacosRecord, requireAutomaticMachine, tryAcquireBuildSlotClaim, unreadableHostedNative } from "@stim-cli/core/state";
|
|
25
|
+
import { basename as basename$1, isAbsolute as isAbsolute$1, join as join$1, relative as relative$1 } from "path";
|
|
24
26
|
import chalk from "chalk";
|
|
25
|
-
import {
|
|
27
|
+
import { existsSync as existsSync$1, lstatSync as lstatSync$1, readdirSync as readdirSync$1, rmSync as rmSync$1, statSync as statSync$1 } from "fs";
|
|
26
28
|
import { resolve4 } from "node:dns/promises";
|
|
27
29
|
import { request } from "node:https";
|
|
30
|
+
//#region src/commands/gc/results.ts
|
|
31
|
+
const results = [];
|
|
32
|
+
function recordGcResult(kind, status, label, { id = null, bytes = null, detail = null } = {}) {
|
|
33
|
+
results.push({
|
|
34
|
+
kind,
|
|
35
|
+
status,
|
|
36
|
+
label,
|
|
37
|
+
id,
|
|
38
|
+
bytes,
|
|
39
|
+
detail
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function takeGcResults() {
|
|
43
|
+
return results.splice(0);
|
|
44
|
+
}
|
|
45
|
+
const GC_OUTPUT = {
|
|
46
|
+
log: (line) => console.log(line),
|
|
47
|
+
record: recordGcResult,
|
|
48
|
+
fail: () => {
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
//#endregion
|
|
28
53
|
//#region src/engine/build-slots.ts
|
|
29
54
|
const SLOT_PREFIX = "slot-";
|
|
30
55
|
const SLOT_POLL_MS = 1e3;
|
|
31
56
|
const SLOT_PROGRESS_MS = 3e4;
|
|
32
57
|
const SLOT_CEILING_MS = 54e5;
|
|
33
|
-
function sleepAsync$1(ms) {
|
|
34
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
35
|
-
}
|
|
36
58
|
function toRecord(holder) {
|
|
37
59
|
const details = holder.details;
|
|
38
60
|
return {
|
|
@@ -56,7 +78,7 @@ function tryAcquireBuildSlot({ max, root = null, logFile = null }) {
|
|
|
56
78
|
}
|
|
57
79
|
});
|
|
58
80
|
if (!got) return null;
|
|
59
|
-
declareSpawnsOn(got.claim);
|
|
81
|
+
declareSpawnsOn(ambientContext(), got.claim);
|
|
60
82
|
return {
|
|
61
83
|
acquired: true,
|
|
62
84
|
path: got.path,
|
|
@@ -74,7 +96,11 @@ function tryAcquireBuildSlot({ max, root = null, logFile = null }) {
|
|
|
74
96
|
function slotWaitingLine({ max, elapsedMs }) {
|
|
75
97
|
return `${"build".padEnd(11)} waiting for a build slot (all ${max} in use, ${formatElapsed(elapsedMs)} elapsed) -- stim guide lifecycle concurrency`;
|
|
76
98
|
}
|
|
77
|
-
async function acquireBuildSlot({ max, root = null, logFile = null, now = Date.now, out = () => {}, waitingFor = () => {},
|
|
99
|
+
async function acquireBuildSlot({ max, root = null, logFile = null, now = Date.now, out = () => {}, waitingFor = () => {}, signal, sleep = (ms) => cancellableSleep(ms, {
|
|
100
|
+
signal,
|
|
101
|
+
what: "build slot"
|
|
102
|
+
}), intervalMs = SLOT_POLL_MS, progressMs = SLOT_PROGRESS_MS, ceilingMs = SLOT_CEILING_MS, automatic = false }) {
|
|
103
|
+
throwIfCancelled(signal, "build slot");
|
|
78
104
|
if (automatic) requireAutomaticMachine("build", "local");
|
|
79
105
|
if (!max || max <= 0) return {
|
|
80
106
|
acquired: true,
|
|
@@ -85,6 +111,7 @@ async function acquireBuildSlot({ max, root = null, logFile = null, now = Date.n
|
|
|
85
111
|
let waited = false;
|
|
86
112
|
try {
|
|
87
113
|
for (;;) {
|
|
114
|
+
throwIfCancelled(signal, "build slot");
|
|
88
115
|
if (automatic) requireAutomaticMachine("build", "local");
|
|
89
116
|
const got = tryAcquireBuildSlot({
|
|
90
117
|
max,
|
|
@@ -123,7 +150,7 @@ async function acquireBuildSlot({ max, root = null, logFile = null, now = Date.n
|
|
|
123
150
|
}
|
|
124
151
|
function releaseBuildSlot(handle) {
|
|
125
152
|
if (!handle || handle.unlimited) return false;
|
|
126
|
-
stopDeclaringSpawnsOn(handle.claim);
|
|
153
|
+
stopDeclaringSpawnsOn(ambientContext(), handle.claim);
|
|
127
154
|
return (0, ownership_claim_exports.releaseClaim)(handle.claim);
|
|
128
155
|
}
|
|
129
156
|
function listBuildSlots() {
|
|
@@ -162,59 +189,49 @@ function listBuildSlots() {
|
|
|
162
189
|
return slots;
|
|
163
190
|
}
|
|
164
191
|
//#endregion
|
|
165
|
-
//#region src/
|
|
166
|
-
function
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
const identityRecord = statePid ? state : record;
|
|
192
|
-
if (statePid && recordPid && state?.processToken !== record?.processToken) return {
|
|
193
|
-
status: "unverified",
|
|
194
|
-
pid,
|
|
195
|
-
reason: "workspace state and registry process identities disagree"
|
|
196
|
-
};
|
|
197
|
-
const identity = inspectIdentity(identityRecord);
|
|
198
|
-
if (identity === "gone" || identity === "different") return {
|
|
199
|
-
status: "stale",
|
|
200
|
-
pid
|
|
201
|
-
};
|
|
202
|
-
if (identity !== "same") return {
|
|
203
|
-
status: "unverified",
|
|
204
|
-
pid,
|
|
205
|
-
reason: "the recorded process identity could not be verified"
|
|
206
|
-
};
|
|
192
|
+
//#region src/devices/report-only-usage.ts
|
|
193
|
+
function canonical$1(path) {
|
|
194
|
+
try {
|
|
195
|
+
return realpathSync.native(path);
|
|
196
|
+
} catch {
|
|
197
|
+
return resolve(path);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function parseDu(output) {
|
|
201
|
+
const sizes = /* @__PURE__ */ new Map();
|
|
202
|
+
for (const line of output.split("\n")) {
|
|
203
|
+
const match = /^(\d+)\t(.+?)\r?$/.exec(line);
|
|
204
|
+
if (match) sizes.set(resolve(match[2]), Number(match[1]) * 1024);
|
|
205
|
+
}
|
|
206
|
+
return sizes;
|
|
207
|
+
}
|
|
208
|
+
async function measureDu(args) {
|
|
209
|
+
let output = "";
|
|
210
|
+
let complete = true;
|
|
211
|
+
try {
|
|
212
|
+
output = await getExecutor().runFileAsync("du", args, { timeoutMs: 2e4 });
|
|
213
|
+
} catch (error) {
|
|
214
|
+
complete = false;
|
|
215
|
+
const failure = error;
|
|
216
|
+
if (failure.status === 1 && !failure.code && typeof failure.stdout === "string") output = failure.stdout;
|
|
217
|
+
}
|
|
207
218
|
return {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
pid,
|
|
211
|
-
port,
|
|
212
|
-
mode: state?.mode ?? record?.mode ?? null,
|
|
213
|
-
startedAt: state?.startedAt ?? record?.startedAt ?? null
|
|
219
|
+
sizes: parseDu(output),
|
|
220
|
+
complete
|
|
214
221
|
};
|
|
215
222
|
}
|
|
216
|
-
function
|
|
217
|
-
|
|
223
|
+
function writeUsageCache(file, value, signal) {
|
|
224
|
+
signal?.throwIfAborted();
|
|
225
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
226
|
+
try {
|
|
227
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
228
|
+
writeFileSync(temporary, JSON.stringify(value));
|
|
229
|
+
renameSync(temporary, file);
|
|
230
|
+
} catch {
|
|
231
|
+
try {
|
|
232
|
+
rmSync(temporary, { force: true });
|
|
233
|
+
} catch {}
|
|
234
|
+
}
|
|
218
235
|
}
|
|
219
236
|
//#endregion
|
|
220
237
|
//#region src/engine/public-http-probe.ts
|
|
@@ -275,1012 +292,545 @@ async function probePublicHttp(url, signal) {
|
|
|
275
292
|
});
|
|
276
293
|
}
|
|
277
294
|
//#endregion
|
|
278
|
-
//#region src/
|
|
279
|
-
function
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
]
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
"
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
]
|
|
296
|
-
};
|
|
297
|
-
return {
|
|
298
|
-
bin: "ngrok",
|
|
299
|
-
args: [
|
|
300
|
-
"http",
|
|
301
|
-
String(port),
|
|
302
|
-
`--log=${logFile || "stdout"}`,
|
|
303
|
-
"--log-format=json",
|
|
304
|
-
...ngrokUrl ? ["--url", ngrokUrl] : []
|
|
305
|
-
]
|
|
306
|
-
};
|
|
295
|
+
//#region src/devices/agent-device-cleanup.ts
|
|
296
|
+
function parseAgentDeviceSessions(output) {
|
|
297
|
+
const result = JSON.parse(output);
|
|
298
|
+
if (result?.success !== true || !Array.isArray(result.data?.sessions)) throw new Error("agent-device did not return a successful session list");
|
|
299
|
+
return result.data.sessions.flatMap((entry) => {
|
|
300
|
+
if (!entry || typeof entry !== "object") return [];
|
|
301
|
+
const row = entry;
|
|
302
|
+
const id = row.platform === "ios" ? row.device_udid : row.id;
|
|
303
|
+
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 [];
|
|
304
|
+
return [{
|
|
305
|
+
name: typeof row.address === "string" && row.address.trim() ? row.address : row.name,
|
|
306
|
+
platform: row.platform,
|
|
307
|
+
id,
|
|
308
|
+
device: typeof row.device === "string" ? row.device : null,
|
|
309
|
+
createdAt: row.createdAt
|
|
310
|
+
}];
|
|
311
|
+
});
|
|
307
312
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
313
|
+
function isOwnDeviceSession(session, device, owner) {
|
|
314
|
+
if (session.platform !== device.platform || device.id !== null && session.id !== device.id) return false;
|
|
315
|
+
if (device.platform === "android" && session.device !== device.avdName.replaceAll("_", " ")) return false;
|
|
316
|
+
if (!owner) return true;
|
|
317
|
+
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));
|
|
312
318
|
}
|
|
313
|
-
function
|
|
314
|
-
|
|
319
|
+
function within(root, path) {
|
|
320
|
+
const rest = relative(root, path);
|
|
321
|
+
return rest === "" || !isAbsolute(rest) && rest.split(sep)[0] !== "..";
|
|
322
|
+
}
|
|
323
|
+
function canonical(path) {
|
|
315
324
|
try {
|
|
316
|
-
|
|
325
|
+
return realpathSync(path);
|
|
317
326
|
} catch {
|
|
318
327
|
return null;
|
|
319
328
|
}
|
|
320
|
-
if (typeof data !== "object" || data === null) return null;
|
|
321
|
-
const url = data.url;
|
|
322
|
-
return typeof url === "string" && /^https?:\/\//i.test(url) ? url : null;
|
|
323
|
-
}
|
|
324
|
-
function parseTailscaleLine(line) {
|
|
325
|
-
return /^https:\/\/[^\s/]+\.ts\.net(:\d+)?\/?$/.test(line) ? line : null;
|
|
326
|
-
}
|
|
327
|
-
function parserFor(provider) {
|
|
328
|
-
if (provider === "tailscale") return parseTailscaleLine;
|
|
329
|
-
return provider === "cloudflared" ? parseCloudflaredLine : parseNgrokLine;
|
|
330
329
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
...options,
|
|
338
|
-
rejectOwnerPurposes: ["workspace removal"]
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
async function withManagedTunnelRemovalLock(root, fn, options = {}) {
|
|
342
|
-
return withWorkspaceProcessLock(managedTunnelLockRoot(root), "metro-tunnel", fn, {
|
|
343
|
-
...options,
|
|
344
|
-
ownerPurpose: "workspace removal"
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
async function withManagedRemoteWorktreeLock(worktreeRoot, fn, options = {}) {
|
|
348
|
-
return withWorkspaceProcessLock(managedRemoteWorktreeLockRoot(worktreeRoot), "managed-remote", fn, {
|
|
349
|
-
...options,
|
|
350
|
-
external: true,
|
|
351
|
-
rejectOwnerPurposes: ["worktree removal"]
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
async function withManagedRemoteWorktreeRemovalLock(worktreeRoot, fn, options = {}) {
|
|
355
|
-
return withWorkspaceProcessLock(managedRemoteWorktreeLockRoot(worktreeRoot), "managed-remote", fn, {
|
|
356
|
-
...options,
|
|
357
|
-
external: true,
|
|
358
|
-
ownerPurpose: "worktree removal"
|
|
359
|
-
});
|
|
330
|
+
function deviceClaims(device) {
|
|
331
|
+
return readAgentDeviceRecords(ambientEnv().HOME || ambientEnv().USERPROFILE || homedir(), ambientEnv()).filter((record) => record.kind === "claim" && (device.id === null || record.deviceId === device.id)).map((record) => ({
|
|
332
|
+
deviceId: record.deviceId,
|
|
333
|
+
session: record.session,
|
|
334
|
+
workspace: record.workspace && canonical(record.workspace)
|
|
335
|
+
}));
|
|
360
336
|
}
|
|
361
|
-
function
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
}];
|
|
369
|
-
const reasons = [];
|
|
370
|
-
for (const { label, path } of locks) {
|
|
371
|
-
const claims = (0, ownership_claim_exports.readClaimSet)(path);
|
|
372
|
-
const holder = claims.live[0];
|
|
373
|
-
if (holder) {
|
|
374
|
-
const purpose = typeof holder.details.purpose === "string" ? ` for ${holder.details.purpose}` : "";
|
|
375
|
-
reasons.push(`the ${label} lock is held${purpose}`);
|
|
376
|
-
} else if (claims.unresolved[0]) reasons.push(`the ${label} lock cannot be resolved: ${claims.unresolved[0].reason}`);
|
|
337
|
+
function explainFailure(error) {
|
|
338
|
+
const stdout = error?.stdout;
|
|
339
|
+
let failure;
|
|
340
|
+
try {
|
|
341
|
+
failure = typeof stdout === "string" ? JSON.parse(stdout)?.error : void 0;
|
|
342
|
+
} catch {
|
|
343
|
+
failure = void 0;
|
|
377
344
|
}
|
|
378
|
-
return
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
const key = createHash("sha256").update(resolve(root)).digest("hex");
|
|
382
|
-
return join(getConfigDir(), "process-locks", key);
|
|
383
|
-
}
|
|
384
|
-
function managedRemoteWorktreeLockRoot(worktreeRoot) {
|
|
385
|
-
const key = createHash("sha256").update(resolve(worktreeRoot)).digest("hex");
|
|
386
|
-
return join(getConfigDir(), "process-locks", "worktrees", key);
|
|
345
|
+
if (typeof failure?.message !== "string" || !failure.message) return error;
|
|
346
|
+
const reason = typeof failure.details?.reason === "string" ? ` (${failure.details.reason})` : "";
|
|
347
|
+
return /* @__PURE__ */ new Error(`agent-device refused: ${failure.message}${reason}`);
|
|
387
348
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
return status !== null && status > 0;
|
|
349
|
+
function report(message) {
|
|
350
|
+
ambientWrite("stderr", `${phaseLine("device", message)}\n`);
|
|
391
351
|
}
|
|
392
|
-
function
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
if (settled) return;
|
|
403
|
-
settled = true;
|
|
404
|
-
clearTimeout(timer);
|
|
405
|
-
child.stdout?.removeListener("data", onOut);
|
|
406
|
-
child.stderr?.removeListener("data", onErr);
|
|
407
|
-
child.removeListener("error", onError);
|
|
408
|
-
child.removeListener("exit", onExit);
|
|
409
|
-
resolve({
|
|
410
|
-
url,
|
|
411
|
-
exited,
|
|
412
|
-
lastLine
|
|
413
|
-
});
|
|
414
|
-
};
|
|
415
|
-
const onLine = (line) => {
|
|
416
|
-
if (settled) return;
|
|
417
|
-
if (line.trim()) lastLine = line.trim();
|
|
418
|
-
const url = parseLine(line);
|
|
419
|
-
if (url) finish(url);
|
|
420
|
-
};
|
|
421
|
-
const outReader = createLineReader(onLine);
|
|
422
|
-
const errReader = createLineReader(onLine);
|
|
423
|
-
onOut = (chunk) => outReader.push(chunk);
|
|
424
|
-
onErr = (chunk) => errReader.push(chunk);
|
|
425
|
-
onError = () => finish(null);
|
|
426
|
-
onExit = () => {
|
|
427
|
-
outReader.flush();
|
|
428
|
-
errReader.flush();
|
|
429
|
-
finish(null, true);
|
|
430
|
-
};
|
|
431
|
-
child.stdout?.setEncoding?.("utf-8");
|
|
432
|
-
child.stderr?.setEncoding?.("utf-8");
|
|
433
|
-
child.stdout?.on("data", onOut);
|
|
434
|
-
child.stderr?.on("data", onErr);
|
|
435
|
-
child.on("error", onError);
|
|
436
|
-
child.on("exit", onExit);
|
|
437
|
-
timer = setTimeout(() => finish(null), timeoutMs);
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
function waitForFileUrl(child, logFile, parseLine, timeoutMs) {
|
|
441
|
-
return new Promise((resolve) => {
|
|
442
|
-
let settled = false;
|
|
443
|
-
const finish = (url, exited = false) => {
|
|
444
|
-
if (settled) return;
|
|
445
|
-
settled = true;
|
|
446
|
-
clearInterval(interval);
|
|
447
|
-
clearTimeout(timer);
|
|
448
|
-
child.removeListener("error", onError);
|
|
449
|
-
child.removeListener("exit", onExit);
|
|
450
|
-
resolve({
|
|
451
|
-
url,
|
|
452
|
-
exited
|
|
453
|
-
});
|
|
352
|
+
async function closeOwnedDeviceSessions(device, stillOwned, workspace, repoRoot) {
|
|
353
|
+
const exec = getExecutor();
|
|
354
|
+
try {
|
|
355
|
+
if (!exec.findExecutable("agent-device")) return;
|
|
356
|
+
const root = workspace === void 0 ? void 0 : canonical(workspace);
|
|
357
|
+
if (root === null) return;
|
|
358
|
+
const owner = root === void 0 ? void 0 : {
|
|
359
|
+
workspace: root,
|
|
360
|
+
repoRoot: repoRoot && (canonical(repoRoot) ?? void 0),
|
|
361
|
+
claims: deviceClaims(device)
|
|
454
362
|
};
|
|
455
|
-
const
|
|
363
|
+
const stateDir = root === void 0 ? void 0 : workspaceAgentDeviceDir(root);
|
|
364
|
+
const stateDirs = [void 0, ...stateDir && !["none", "dead"].includes(agentDeviceDaemon(stateDir)) ? [stateDir] : []];
|
|
365
|
+
for (const dir of stateDirs) {
|
|
366
|
+
const deadline = Date.now() + 15e3;
|
|
367
|
+
const run = async (args) => {
|
|
368
|
+
const remaining = deadline - Date.now();
|
|
369
|
+
if (remaining <= 0) throw new Error("agent-device cleanup timed out");
|
|
370
|
+
try {
|
|
371
|
+
return await exec.runFileAsync("agent-device", [
|
|
372
|
+
...args,
|
|
373
|
+
...dir ? ["--state-dir", dir] : [],
|
|
374
|
+
"--json",
|
|
375
|
+
"--daemon-transport",
|
|
376
|
+
"socket"
|
|
377
|
+
], {
|
|
378
|
+
timeoutMs: Math.min(5e3, remaining),
|
|
379
|
+
killSignal: "SIGKILL"
|
|
380
|
+
});
|
|
381
|
+
} catch (error) {
|
|
382
|
+
throw explainFailure(error);
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
const list = async () => parseAgentDeviceSessions(await run([
|
|
386
|
+
"session",
|
|
387
|
+
"list",
|
|
388
|
+
"--session",
|
|
389
|
+
"stim-teardown-inventory"
|
|
390
|
+
]));
|
|
456
391
|
try {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
if (
|
|
392
|
+
const sessions = (await list()).filter((session) => isOwnDeviceSession(session, device, owner));
|
|
393
|
+
for (const session of sessions) try {
|
|
394
|
+
if (!(await list()).some((current) => current.name === session.name && current.platform === session.platform && current.id === session.id && current.createdAt === session.createdAt)) continue;
|
|
395
|
+
if (!await stillOwned(session.id)) continue;
|
|
396
|
+
if (JSON.parse(await run([
|
|
397
|
+
"close",
|
|
398
|
+
"--session",
|
|
399
|
+
session.name,
|
|
400
|
+
"--session-lock",
|
|
401
|
+
"reject",
|
|
402
|
+
"--platform",
|
|
403
|
+
device.platform,
|
|
404
|
+
device.platform === "ios" ? "--udid" : "--serial",
|
|
405
|
+
session.id
|
|
406
|
+
]))?.success !== true) throw new Error("agent-device did not confirm session close");
|
|
407
|
+
report(`closed agent-device session ${session.name} on ${session.id}`);
|
|
408
|
+
} catch (error) {
|
|
409
|
+
report(`could not close agent-device session ${session.name} on ${session.id}: ${error.message}`);
|
|
460
410
|
}
|
|
461
|
-
} catch {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const timer = setTimeout(() => finish(null), timeoutMs);
|
|
469
|
-
probe();
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
function createTunnelLogFile(provider) {
|
|
473
|
-
const dir = join(getConfigDir(), "tunnel-logs");
|
|
474
|
-
mkdirSync(dir, { recursive: true });
|
|
475
|
-
const path = join(dir, `${provider}-${randomUUID()}.log`);
|
|
476
|
-
closeSync(openSync(path, "wx", 384));
|
|
477
|
-
return path;
|
|
411
|
+
} catch (error) {
|
|
412
|
+
report(`could not list agent-device sessions for ${device.platform === "ios" ? device.id : device.id ?? device.avdName}: ${error.message}`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
} catch (error) {
|
|
416
|
+
report(`could not list agent-device sessions for ${device.platform === "ios" ? device.id : device.id ?? device.avdName}: ${error.message}`);
|
|
417
|
+
}
|
|
478
418
|
}
|
|
479
|
-
|
|
480
|
-
|
|
419
|
+
//#endregion
|
|
420
|
+
//#region src/devices/teardown.ts
|
|
421
|
+
async function teardownParkedAvd(avdName) {
|
|
481
422
|
try {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
423
|
+
return await removeParkedAfter("android", avdName, async () => {
|
|
424
|
+
const result = await teardownOwnedAvd(avdName, {
|
|
425
|
+
del: true,
|
|
426
|
+
owner: { pool: true }
|
|
427
|
+
});
|
|
428
|
+
if (result.status !== "torn-down" && result.status !== "missing") throw new Error(result.reason);
|
|
429
|
+
}) ? {
|
|
430
|
+
status: "torn-down",
|
|
431
|
+
label: avdName
|
|
432
|
+
} : {
|
|
433
|
+
status: "skipped",
|
|
434
|
+
kind: "not-parked",
|
|
435
|
+
reason: "emulator is no longer parked"
|
|
436
|
+
};
|
|
437
|
+
} catch (error) {
|
|
438
|
+
return {
|
|
439
|
+
status: "failed",
|
|
440
|
+
reason: String(error?.message || error)
|
|
441
|
+
};
|
|
498
442
|
}
|
|
499
443
|
}
|
|
500
|
-
|
|
501
|
-
const CLEANUP_POLL_MS = 25;
|
|
502
|
-
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 }) {
|
|
503
|
-
const spawn = spawnFn || ((cmd, args, opts) => getExecutor().spawn(cmd, args, opts));
|
|
504
|
-
const outputFile = provider === "tailscale" ? null : logFile || (spawnFn ? null : createTunnelLogFile(provider));
|
|
505
|
-
let child;
|
|
444
|
+
async function teardownParkedIosSim(udid, { label, deleteSim = deleteParkedIosSim } = {}) {
|
|
506
445
|
try {
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
"ignore",
|
|
514
|
-
"ignore"
|
|
515
|
-
] : [
|
|
516
|
-
"ignore",
|
|
517
|
-
"pipe",
|
|
518
|
-
"pipe"
|
|
519
|
-
],
|
|
520
|
-
detached: true
|
|
446
|
+
const removed = await removeParkedAfter("ios", udid, async () => {
|
|
447
|
+
await closeOwnedDeviceSessions({
|
|
448
|
+
platform: "ios",
|
|
449
|
+
id: udid
|
|
450
|
+
}, async () => Boolean((await resolveOwnedIosSimNow(udid)).sim));
|
|
451
|
+
await deleteSim(udid);
|
|
521
452
|
});
|
|
522
|
-
|
|
523
|
-
|
|
453
|
+
if (!removed) return {
|
|
454
|
+
status: "skipped",
|
|
455
|
+
kind: "not-parked",
|
|
456
|
+
reason: "simulator is no longer parked"
|
|
457
|
+
};
|
|
524
458
|
return {
|
|
525
|
-
|
|
526
|
-
|
|
459
|
+
status: "torn-down",
|
|
460
|
+
label: label ?? removed.name
|
|
461
|
+
};
|
|
462
|
+
} catch (error) {
|
|
463
|
+
return {
|
|
464
|
+
status: "failed",
|
|
465
|
+
reason: String(error?.message || error)
|
|
527
466
|
};
|
|
528
467
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
468
|
+
}
|
|
469
|
+
async function eraseParkedIosSim(udid, { label } = {}) {
|
|
470
|
+
try {
|
|
471
|
+
const erased = await eraseParkedAfter("ios", udid, async (_record, eraseStarted) => {
|
|
472
|
+
await closeOwnedDeviceSessions({
|
|
473
|
+
platform: "ios",
|
|
474
|
+
id: udid
|
|
475
|
+
}, async () => Boolean((await resolveOwnedIosSimNow(udid)).sim));
|
|
476
|
+
const resolved = resolveOwnedIosSim(udid, await listAllIosSimsAsync());
|
|
477
|
+
if (resolved.missing) throw new Error(`simulator ${udid} is not on this machine`);
|
|
478
|
+
if (resolved.notOwned) throw new Error(`simulator ${udid} is now named ${JSON.stringify(resolved.notOwned)}`);
|
|
479
|
+
const state = resolved.sim.state;
|
|
480
|
+
if (state !== "Shutdown") throw new Error(`simulator ${udid} is ${state}, not Shutdown; it was kept`);
|
|
481
|
+
eraseStarted();
|
|
482
|
+
await eraseIosSim(udid);
|
|
540
483
|
});
|
|
541
|
-
if (
|
|
542
|
-
|
|
484
|
+
if (!erased) return {
|
|
485
|
+
status: "skipped",
|
|
486
|
+
kind: "not-parked",
|
|
487
|
+
reason: "simulator is no longer parked"
|
|
488
|
+
};
|
|
489
|
+
return {
|
|
490
|
+
status: "torn-down",
|
|
491
|
+
label: label ?? erased.name
|
|
492
|
+
};
|
|
493
|
+
} catch (error) {
|
|
494
|
+
return {
|
|
543
495
|
status: "failed",
|
|
544
|
-
reason:
|
|
496
|
+
reason: String(error?.message || error)
|
|
545
497
|
};
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
async function eraseParkedAvd(avdName) {
|
|
501
|
+
try {
|
|
502
|
+
return await eraseParkedAfter("android", avdName, async (_record, eraseStarted) => {
|
|
503
|
+
if ((await resolveOwnedAvdSerialAsync(avdName, { timeoutMs: AVD_INVENTORY_TIMEOUT_MS })).serial) throw new Error(`AVD ${avdName} is running; it was kept`);
|
|
504
|
+
const result = await teardownOwnedAvd(avdName, {
|
|
505
|
+
wipe: true,
|
|
506
|
+
owner: { pool: true },
|
|
507
|
+
onWipeStart: eraseStarted
|
|
508
|
+
});
|
|
509
|
+
if (result.status !== "torn-down") throw new Error(result.reason ?? `AVD ${avdName} is ${result.status}`);
|
|
510
|
+
}) ? {
|
|
511
|
+
status: "torn-down",
|
|
512
|
+
label: avdName
|
|
552
513
|
} : {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
514
|
+
status: "skipped",
|
|
515
|
+
kind: "not-parked",
|
|
516
|
+
reason: "emulator is no longer parked"
|
|
556
517
|
};
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
try {
|
|
562
|
-
return readProcessToken(pid);
|
|
563
|
-
} catch {
|
|
564
|
-
return null;
|
|
565
|
-
}
|
|
566
|
-
};
|
|
567
|
-
const initialProcessToken = captureChildIdentity();
|
|
568
|
-
if (!initialProcessToken) return failAfterCleanup(`${provider} started but its process identity token could not be read.`);
|
|
569
|
-
const { url, exited, lastLine } = outputFile ? await waitForFileUrl(child, outputFile, parserFor(provider), urlTimeoutMs) : await waitForUrl(child, parserFor(provider), urlTimeoutMs);
|
|
570
|
-
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);
|
|
571
|
-
if (!outputFile) resumeChildPipes(child);
|
|
572
|
-
if (requireReachable) {
|
|
573
|
-
if (!await waitUntilReachable({
|
|
574
|
-
url,
|
|
575
|
-
now,
|
|
576
|
-
sleep,
|
|
577
|
-
probe: probeReachable,
|
|
578
|
-
timeoutMs: reachableTimeoutMs
|
|
579
|
-
})) return failAfterCleanup(`${url} did not become reachable within ${reachableTimeoutMs}ms; ${provider} may still be registering, or the network path is blocked.`);
|
|
580
|
-
}
|
|
581
|
-
const finalProcessToken = captureChildIdentity();
|
|
582
|
-
if (!finalProcessToken || finalProcessToken !== initialProcessToken) return failAfterCleanup(`${provider} process identity changed before its tunnel could be recorded.`);
|
|
583
|
-
if (!outputFile) unrefChildPipes(child);
|
|
584
|
-
child.unref?.();
|
|
585
|
-
if (childExited) return failAfterCleanup(`${provider} exited before its tunnel could be recorded.`, true);
|
|
586
|
-
return {
|
|
587
|
-
url,
|
|
588
|
-
pid,
|
|
589
|
-
processToken: initialProcessToken,
|
|
590
|
-
logFile: outputFile,
|
|
591
|
-
cleanup: () => cleanup()
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
async function startTunnelSequence({ providers, port, ngrokUrl = null, requireReachable = true, start = startTunnel }) {
|
|
595
|
-
const failures = [];
|
|
596
|
-
for (const provider of providers) {
|
|
597
|
-
const result = await start({
|
|
598
|
-
provider,
|
|
599
|
-
port,
|
|
600
|
-
ngrokUrl: provider === "ngrok" ? ngrokUrl : null,
|
|
601
|
-
requireReachable
|
|
602
|
-
});
|
|
603
|
-
if (!("failed" in result)) return {
|
|
604
|
-
provider,
|
|
605
|
-
...result
|
|
606
|
-
};
|
|
607
|
-
failures.push(`${provider}: ${result.reason}`);
|
|
608
|
-
if (result.cleanupFailed) return {
|
|
609
|
-
failed: true,
|
|
610
|
-
reason: failures.join(" ")
|
|
518
|
+
} catch (error) {
|
|
519
|
+
return {
|
|
520
|
+
status: "failed",
|
|
521
|
+
reason: String(error?.message || error)
|
|
611
522
|
};
|
|
612
523
|
}
|
|
613
|
-
return {
|
|
614
|
-
failed: true,
|
|
615
|
-
reason: failures.join(" ") || "No managed tunnel provider was selected."
|
|
616
|
-
};
|
|
617
|
-
}
|
|
618
|
-
function closeChildPipes(child) {
|
|
619
|
-
child.stdout?.destroy?.();
|
|
620
|
-
child.stderr?.destroy?.();
|
|
621
|
-
}
|
|
622
|
-
function unrefChildPipes(child) {
|
|
623
|
-
resumeChildPipes(child);
|
|
624
|
-
for (const stream of [child.stdout, child.stderr]) stream?.unref?.();
|
|
625
|
-
}
|
|
626
|
-
function resumeChildPipes(child) {
|
|
627
|
-
child.stdout?.resume?.();
|
|
628
|
-
child.stderr?.resume?.();
|
|
629
524
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
525
|
+
const IOS_SHUTDOWN_SETTLE_MS = 15e3;
|
|
526
|
+
const ADB_RELEASE_WAIT_MS = 3e3;
|
|
527
|
+
const AVD_INVENTORY_TIMEOUT_MS = 15e3;
|
|
528
|
+
async function repoRootAbove(workspace) {
|
|
529
|
+
if (workspace === void 0 || !getExecutor().findExecutable("agent-device")) return void 0;
|
|
530
|
+
const root = await repoRootAsync(workspace);
|
|
636
531
|
try {
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
child.removeListener("exit", onExit);
|
|
641
|
-
return isEsrch(err);
|
|
532
|
+
return root && realpathSync(root) !== realpathSync(workspace) ? root : void 0;
|
|
533
|
+
} catch {
|
|
534
|
+
return;
|
|
642
535
|
}
|
|
643
|
-
const hasExited = () => exited;
|
|
644
|
-
const deadline = now() + timeoutMs;
|
|
645
|
-
while (!hasExited() && now() < deadline) await sleep(CLEANUP_POLL_MS);
|
|
646
|
-
if (!hasExited() && child.pid && !isAlive(child.pid)) exited = true;
|
|
647
|
-
child.removeListener("exit", onExit);
|
|
648
|
-
return hasExited();
|
|
649
536
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
}
|
|
655
|
-
if (await signalAndWaitForExit(child, "SIGTERM", {
|
|
656
|
-
timeoutMs,
|
|
657
|
-
now,
|
|
658
|
-
sleep,
|
|
659
|
-
isAlive,
|
|
660
|
-
platform
|
|
661
|
-
})) {
|
|
662
|
-
closeChildPipes(child);
|
|
663
|
-
return true;
|
|
664
|
-
}
|
|
665
|
-
const killed = await signalAndWaitForExit(child, "SIGKILL", {
|
|
666
|
-
timeoutMs,
|
|
667
|
-
now,
|
|
668
|
-
sleep,
|
|
669
|
-
isAlive,
|
|
670
|
-
platform
|
|
671
|
-
});
|
|
672
|
-
closeChildPipes(child);
|
|
673
|
-
return killed;
|
|
537
|
+
const IOS_SHUTDOWN_POLL_MS = 250;
|
|
538
|
+
const IOS_SHUTDOWN_ATTEMPTS = 2;
|
|
539
|
+
async function resolveOwnedIosSimNow(udid) {
|
|
540
|
+
return resolveOwnedIosSim(udid, await listAllIosSimsAsync());
|
|
674
541
|
}
|
|
675
|
-
function
|
|
676
|
-
|
|
542
|
+
async function settleIosSimShutdown(udid, { now = Date.now, sleep = sleepAsync }) {
|
|
543
|
+
let resolved = await resolveOwnedIosSimNow(udid);
|
|
544
|
+
for (let attempt = 1;; attempt++) {
|
|
545
|
+
const deadline = now() + IOS_SHUTDOWN_SETTLE_MS;
|
|
546
|
+
while (resolved.sim && resolved.sim.state !== "Shutdown" && now() < deadline) {
|
|
547
|
+
await sleep(IOS_SHUTDOWN_POLL_MS);
|
|
548
|
+
resolved = await resolveOwnedIosSimNow(udid);
|
|
549
|
+
}
|
|
550
|
+
if (!resolved.sim || resolved.sim.state === "Shutdown" || attempt === IOS_SHUTDOWN_ATTEMPTS) return resolved;
|
|
551
|
+
await shutdownIosSim(udid);
|
|
552
|
+
resolved = await resolveOwnedIosSimNow(udid);
|
|
553
|
+
}
|
|
677
554
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
function isEsrch(err) {
|
|
681
|
-
return err?.code === "ESRCH";
|
|
555
|
+
function iosShutdownFailureReason(udid, state) {
|
|
556
|
+
return `simulator ${udid} is still ${state} after ${IOS_SHUTDOWN_ATTEMPTS} shutdown attempts and ${IOS_SHUTDOWN_ATTEMPTS * IOS_SHUTDOWN_SETTLE_MS / 1e3}s of waiting`;
|
|
682
557
|
}
|
|
683
|
-
async function
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
558
|
+
async function parkOwnedIosSim(settled, udid, park) {
|
|
559
|
+
if (settled.missing) throw new Error(`simulator ${udid} disappeared after shutdown`);
|
|
560
|
+
if (settled.notOwned) throw new Error(`simulator ${udid} is now named ${JSON.stringify(settled.notOwned)}`);
|
|
561
|
+
const sim = settled.sim;
|
|
562
|
+
if (sim.state !== "Shutdown") throw new Error(iosShutdownFailureReason(udid, sim.state));
|
|
563
|
+
const model = (await listIosDeviceTypesAsync()).find((d) => d.identifier === sim.deviceTypeIdentifier)?.name ?? null;
|
|
564
|
+
const runtime = parseRuntimeVersion(sim.runtime);
|
|
565
|
+
const name = parkedSimName(sim.udid, {
|
|
566
|
+
model,
|
|
567
|
+
runtime
|
|
568
|
+
});
|
|
569
|
+
await renameIosSim(sim.udid, name);
|
|
570
|
+
const record = {
|
|
571
|
+
udid: sim.udid,
|
|
572
|
+
name,
|
|
573
|
+
deviceTypeIdentifier: sim.deviceTypeIdentifier,
|
|
574
|
+
runtimeIdentifier: sim.runtime,
|
|
575
|
+
parkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
576
|
+
simslimManaged: Boolean(park.simslimManaged),
|
|
577
|
+
...park.appId ? { bundleId: park.appId } : {},
|
|
578
|
+
...park.cacheKey ? { cacheKey: park.cacheKey } : {}
|
|
693
579
|
};
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
580
|
+
return {
|
|
581
|
+
record,
|
|
582
|
+
evicted: parkSim({
|
|
583
|
+
platform: "ios",
|
|
584
|
+
projectPath: park.projectPath,
|
|
585
|
+
slot: park.slot,
|
|
586
|
+
record,
|
|
587
|
+
max: park.max
|
|
588
|
+
})
|
|
699
589
|
};
|
|
590
|
+
}
|
|
591
|
+
async function teardownOwnedIosSim(udid, { del = false, label, park, workspace, shutdownClock = {} } = {}) {
|
|
592
|
+
let parkFallback;
|
|
700
593
|
try {
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
594
|
+
const resolved = await resolveOwnedIosSimNow(udid);
|
|
595
|
+
if (resolved.notOwned) return {
|
|
596
|
+
status: "skipped",
|
|
597
|
+
kind: "not-owned",
|
|
598
|
+
reason: `sim "${resolved.notOwned}" is not Stim-owned: Stim has no record of creating it`
|
|
599
|
+
};
|
|
600
|
+
if (resolved.missing) return { status: "missing" };
|
|
601
|
+
await closeOwnedDeviceSessions({
|
|
602
|
+
platform: "ios",
|
|
603
|
+
id: udid
|
|
604
|
+
}, async () => Boolean((await resolveOwnedIosSimNow(udid)).sim), workspace);
|
|
605
|
+
const current = await resolveOwnedIosSimNow(udid);
|
|
606
|
+
if (current.missing) return { status: "missing" };
|
|
607
|
+
if (current.notOwned) return {
|
|
608
|
+
status: "skipped",
|
|
609
|
+
kind: "not-owned",
|
|
610
|
+
reason: "simulator ownership changed before shutdown"
|
|
611
|
+
};
|
|
612
|
+
await shutdownIosSim(udid);
|
|
613
|
+
const sim = resolved.sim;
|
|
614
|
+
if (del && park && park.max > 0) try {
|
|
615
|
+
const { record, evicted } = await parkOwnedIosSim(await settleIosSimShutdown(udid, shutdownClock), udid, park);
|
|
616
|
+
const removed = [];
|
|
617
|
+
const failures = [];
|
|
618
|
+
for (const entry of evicted) {
|
|
619
|
+
const result = await teardownParkedIosSim(entry.udid, { label: entry.name });
|
|
620
|
+
if (result.status === "torn-down") removed.push({
|
|
621
|
+
udid: entry.udid,
|
|
622
|
+
name: entry.name
|
|
623
|
+
});
|
|
624
|
+
else if (result.status === "failed") failures.push(`could not delete evicted ${entry.name} (${entry.udid}): ${result.reason}`);
|
|
625
|
+
}
|
|
626
|
+
return {
|
|
627
|
+
status: "torn-down",
|
|
628
|
+
label: label ?? sim.name ?? udid,
|
|
629
|
+
parked: {
|
|
630
|
+
udid: record.udid,
|
|
631
|
+
name: record.name
|
|
632
|
+
},
|
|
633
|
+
evicted: removed,
|
|
634
|
+
...failures.length ? { evictionFailures: failures } : {}
|
|
635
|
+
};
|
|
636
|
+
} catch (e) {
|
|
637
|
+
parkFallback = String(e?.message || e);
|
|
638
|
+
}
|
|
639
|
+
if (del) {
|
|
640
|
+
await deleteIosSim(udid);
|
|
641
|
+
return {
|
|
642
|
+
status: "torn-down",
|
|
643
|
+
label: label ?? sim.name ?? udid,
|
|
644
|
+
...parkFallback ? { parkFallback } : {}
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
const settled = await settleIosSimShutdown(udid, shutdownClock);
|
|
648
|
+
if (settled.sim && settled.sim.state !== "Shutdown") return {
|
|
704
649
|
status: "failed",
|
|
705
|
-
|
|
650
|
+
label: label ?? sim.name ?? udid,
|
|
651
|
+
reason: iosShutdownFailureReason(udid, settled.sim.state)
|
|
652
|
+
};
|
|
653
|
+
const root = sim.state === "Shutdown" ? void 0 : await repoRootAbove(workspace);
|
|
654
|
+
if (root && settled.sim) await closeOwnedDeviceSessions({
|
|
655
|
+
platform: "ios",
|
|
656
|
+
id: udid
|
|
657
|
+
}, async () => (await resolveOwnedIosSimNow(udid)).sim?.state === "Shutdown", workspace, root);
|
|
658
|
+
return {
|
|
659
|
+
status: "torn-down",
|
|
660
|
+
label: label ?? sim.name ?? udid
|
|
661
|
+
};
|
|
662
|
+
} catch (e) {
|
|
663
|
+
return {
|
|
664
|
+
status: "failed",
|
|
665
|
+
reason: String(e?.message || e),
|
|
666
|
+
...parkFallback ? { parkFallback } : {}
|
|
706
667
|
};
|
|
707
|
-
}
|
|
708
|
-
const deadline = now() + timeoutMs;
|
|
709
|
-
const exited = () => {
|
|
710
|
-
if (!isAlive(pid)) return true;
|
|
711
|
-
const current = inspectIdentity(record);
|
|
712
|
-
return current === "gone" || current === "different";
|
|
713
|
-
};
|
|
714
|
-
while (now() < deadline && !exited()) await sleep(STOP_POLL_MS);
|
|
715
|
-
if (!exited()) return {
|
|
716
|
-
status: "failed",
|
|
717
|
-
reason: `pid ${pid} did not exit within ${timeoutMs}ms.`
|
|
718
|
-
};
|
|
719
|
-
removeRecordedTunnelLogFile(record.logFile);
|
|
720
|
-
return { status: "stopped" };
|
|
721
|
-
}
|
|
722
|
-
//#endregion
|
|
723
|
-
//#region src/commands/gc/paths.ts
|
|
724
|
-
function canonicalPath(p) {
|
|
725
|
-
try {
|
|
726
|
-
return realpathSync$1(p);
|
|
727
|
-
} catch {
|
|
728
|
-
return resolve$1(p);
|
|
729
668
|
}
|
|
730
669
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
bytes,
|
|
741
|
-
detail
|
|
742
|
-
});
|
|
743
|
-
}
|
|
744
|
-
function takeGcResults() {
|
|
745
|
-
return results.splice(0);
|
|
670
|
+
function assertAvdReferences(avdName, owner) {
|
|
671
|
+
const config = loadConfig$1();
|
|
672
|
+
if (!config) throw new Error("Cannot verify AVD references without a Stim config; the data was kept.");
|
|
673
|
+
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.`);
|
|
674
|
+
for (const [projectPath, project] of Object.entries(config.projects)) for (const { slot, platforms } of projectDeviceSlots(project)) {
|
|
675
|
+
if (platforms.android?.avdName?.toLowerCase() !== avdName.toLowerCase()) continue;
|
|
676
|
+
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.`);
|
|
677
|
+
}
|
|
678
|
+
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.`);
|
|
746
679
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
if (!
|
|
754
|
-
|
|
755
|
-
const
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
}
|
|
680
|
+
function removeOrphanedAvdDirectory(candidate, owner) {
|
|
681
|
+
const surveyed = listOrphanedAvdDirectories(candidate.name).find((entry) => entry.directory === candidate.directory);
|
|
682
|
+
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.`);
|
|
683
|
+
assertOwnedAvdStopped(candidate.name, { resolveDirectory: () => candidate.directory });
|
|
684
|
+
const detached = withConfigLock$1(() => {
|
|
685
|
+
const current = lstatSync(candidate.directory);
|
|
686
|
+
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.`);
|
|
687
|
+
assertAvdReferences(candidate.name, owner);
|
|
688
|
+
const name = `stim-gc-${randomUUID()}`;
|
|
689
|
+
recordCreatedDevice("android", name);
|
|
690
|
+
const destination = join(dirname(candidate.directory), `${name}.avd`);
|
|
691
|
+
try {
|
|
692
|
+
renameSync(candidate.directory, destination);
|
|
693
|
+
} catch (error) {
|
|
694
|
+
forgetCreatedDevice("android", name);
|
|
695
|
+
throw error;
|
|
696
|
+
}
|
|
697
|
+
forgetCreatedDevice("android", candidate.name);
|
|
698
|
+
return destination;
|
|
764
699
|
});
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
if (session.platform !== device.platform || device.id !== null && session.id !== device.id) return false;
|
|
768
|
-
if (device.platform === "android" && session.device !== device.avdName.replaceAll("_", " ")) return false;
|
|
769
|
-
if (!owner) return true;
|
|
770
|
-
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));
|
|
771
|
-
}
|
|
772
|
-
function within(root, path) {
|
|
773
|
-
const rest = relative(root, path);
|
|
774
|
-
return rest === "" || !isAbsolute(rest) && rest.split(sep)[0] !== "..";
|
|
775
|
-
}
|
|
776
|
-
function canonical(path) {
|
|
700
|
+
const current = lstatSync(detached);
|
|
701
|
+
if (!current.isDirectory() || current.dev !== candidate.dev || current.ino !== candidate.ino) throw new Error(`Detached AVD data changed at ${detached}; it was kept.`);
|
|
777
702
|
try {
|
|
778
|
-
|
|
779
|
-
} catch {
|
|
780
|
-
|
|
703
|
+
rmSync(detached, { recursive: true });
|
|
704
|
+
} catch (error) {
|
|
705
|
+
throw new Error(`Could not remove AVD data at ${detached}; retry gc --delete: ${error.message}`, { cause: error });
|
|
781
706
|
}
|
|
707
|
+
forgetCreatedDevice("android", basename(detached, ".avd"));
|
|
782
708
|
}
|
|
783
|
-
function
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
}));
|
|
789
|
-
}
|
|
790
|
-
function explainFailure(error) {
|
|
791
|
-
const stdout = error?.stdout;
|
|
792
|
-
let failure;
|
|
793
|
-
try {
|
|
794
|
-
failure = typeof stdout === "string" ? JSON.parse(stdout)?.error : void 0;
|
|
795
|
-
} catch {
|
|
796
|
-
failure = void 0;
|
|
709
|
+
function teardownUnregisteredAvd(avdName, del, owner, surveyed) {
|
|
710
|
+
const candidates = surveyed ? [surveyed] : listOrphanedAvdDirectories(avdName);
|
|
711
|
+
if (candidates.length === 0) {
|
|
712
|
+
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.`);
|
|
713
|
+
return { status: "missing" };
|
|
797
714
|
}
|
|
798
|
-
if (
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
function closeOwnedDeviceSessions(device, stillOwned, workspace, repoRoot) {
|
|
806
|
-
const exec = getExecutor();
|
|
807
|
-
try {
|
|
808
|
-
if (!exec.findExecutable("agent-device")) return;
|
|
809
|
-
const root = workspace === void 0 ? void 0 : canonical(workspace);
|
|
810
|
-
if (root === null) return;
|
|
811
|
-
const owner = root === void 0 ? void 0 : {
|
|
812
|
-
workspace: root,
|
|
813
|
-
repoRoot: repoRoot && (canonical(repoRoot) ?? void 0),
|
|
814
|
-
claims: deviceClaims(device)
|
|
815
|
-
};
|
|
816
|
-
const stateDir = root === void 0 ? void 0 : workspaceAgentDeviceDir(root);
|
|
817
|
-
const stateDirs = [void 0, ...stateDir && !["none", "dead"].includes(agentDeviceDaemon(stateDir)) ? [stateDir] : []];
|
|
818
|
-
for (const dir of stateDirs) {
|
|
819
|
-
const deadline = Date.now() + 15e3;
|
|
820
|
-
const run = (args) => {
|
|
821
|
-
const remaining = deadline - Date.now();
|
|
822
|
-
if (remaining <= 0) throw new Error("agent-device cleanup timed out");
|
|
823
|
-
try {
|
|
824
|
-
return exec.runFile("agent-device", [
|
|
825
|
-
...args,
|
|
826
|
-
...dir ? ["--state-dir", dir] : [],
|
|
827
|
-
"--json",
|
|
828
|
-
"--daemon-transport",
|
|
829
|
-
"socket"
|
|
830
|
-
], {
|
|
831
|
-
timeoutMs: Math.min(5e3, remaining),
|
|
832
|
-
killSignal: "SIGKILL"
|
|
833
|
-
});
|
|
834
|
-
} catch (error) {
|
|
835
|
-
throw explainFailure(error);
|
|
836
|
-
}
|
|
837
|
-
};
|
|
838
|
-
const list = () => parseAgentDeviceSessions(run([
|
|
839
|
-
"session",
|
|
840
|
-
"list",
|
|
841
|
-
"--session",
|
|
842
|
-
"stim-teardown-inventory"
|
|
843
|
-
]));
|
|
844
|
-
try {
|
|
845
|
-
const sessions = list().filter((session) => isOwnDeviceSession(session, device, owner));
|
|
846
|
-
for (const session of sessions) try {
|
|
847
|
-
if (!list().some((current) => current.name === session.name && current.platform === session.platform && current.id === session.id && current.createdAt === session.createdAt)) continue;
|
|
848
|
-
if (!stillOwned(session.id)) continue;
|
|
849
|
-
if (JSON.parse(run([
|
|
850
|
-
"close",
|
|
851
|
-
"--session",
|
|
852
|
-
session.name,
|
|
853
|
-
"--session-lock",
|
|
854
|
-
"reject",
|
|
855
|
-
"--platform",
|
|
856
|
-
device.platform,
|
|
857
|
-
device.platform === "ios" ? "--udid" : "--serial",
|
|
858
|
-
session.id
|
|
859
|
-
]))?.success !== true) throw new Error("agent-device did not confirm session close");
|
|
860
|
-
report(`closed agent-device session ${session.name} on ${session.id}`);
|
|
861
|
-
} catch (error) {
|
|
862
|
-
report(`could not close agent-device session ${session.name} on ${session.id}: ${error.message}`);
|
|
863
|
-
}
|
|
864
|
-
} catch (error) {
|
|
865
|
-
report(`could not list agent-device sessions for ${device.platform === "ios" ? device.id : device.id ?? device.avdName}: ${error.message}`);
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
} catch (error) {
|
|
869
|
-
report(`could not list agent-device sessions for ${device.platform === "ios" ? device.id : device.id ?? device.avdName}: ${error.message}`);
|
|
715
|
+
if (!del) return {
|
|
716
|
+
status: "skipped",
|
|
717
|
+
reason: "AVD registration is missing; its data was kept."
|
|
718
|
+
};
|
|
719
|
+
for (const candidate of candidates) {
|
|
720
|
+
if (candidate.name !== avdName) throw new Error(`AVD data does not match ${avdName}; it was kept.`);
|
|
721
|
+
removeOrphanedAvdDirectory(candidate, owner);
|
|
870
722
|
}
|
|
723
|
+
return {
|
|
724
|
+
status: "torn-down",
|
|
725
|
+
label: avdName
|
|
726
|
+
};
|
|
871
727
|
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
function teardownParkedAvd(avdName) {
|
|
728
|
+
async function teardownOwnedAvd(avdName, options = {}) {
|
|
729
|
+
let claim;
|
|
875
730
|
try {
|
|
876
|
-
|
|
877
|
-
const result = teardownOwnedAvd(avdName, {
|
|
878
|
-
del: true,
|
|
879
|
-
owner: { pool: true }
|
|
880
|
-
});
|
|
881
|
-
if (result.status !== "torn-down" && result.status !== "missing") throw new Error(result.reason);
|
|
882
|
-
}) ? {
|
|
883
|
-
status: "torn-down",
|
|
884
|
-
label: avdName
|
|
885
|
-
} : {
|
|
731
|
+
if (!isStimOwnedAvd(avdName)) return {
|
|
886
732
|
status: "skipped",
|
|
887
|
-
kind: "not-
|
|
888
|
-
reason:
|
|
733
|
+
kind: "not-owned",
|
|
734
|
+
reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
|
|
889
735
|
};
|
|
736
|
+
claim = acquireAvdClaim(avdName);
|
|
737
|
+
const result = await teardownClaimedAvd(avdName, claim, options);
|
|
738
|
+
if (result.status === "torn-down" || result.status === "missing") options.onRemoved?.();
|
|
739
|
+
return result;
|
|
890
740
|
} catch (error) {
|
|
891
741
|
return {
|
|
892
742
|
status: "failed",
|
|
893
743
|
reason: String(error?.message || error)
|
|
894
744
|
};
|
|
745
|
+
} finally {
|
|
746
|
+
(0, ownership_claim_exports.releaseClaim)(claim);
|
|
895
747
|
}
|
|
896
748
|
}
|
|
897
|
-
function
|
|
749
|
+
async function serialFree(serial, timeoutMs = AVD_INVENTORY_TIMEOUT_MS) {
|
|
750
|
+
const adb = await listAdbDevicesAsync({ timeoutMs });
|
|
751
|
+
return ![
|
|
752
|
+
...adb.emulators,
|
|
753
|
+
...adb.physical,
|
|
754
|
+
...adb.unhealthy
|
|
755
|
+
].some((entry) => entry.serial === serial);
|
|
756
|
+
}
|
|
757
|
+
async function teardownClaimedAvd(avdName, claim, { del = false, wipe = false, park, owner, workspace, orphanedDirectory, onlyIfMissing = false, onWipeStart, waitForShutdown = waitForAndroidEmulatorShutdown, assertStopped = assertOwnedAvdStopped, resolveAvd = (name) => resolveOwnedAvdSerialAsync(name, { timeoutMs: AVD_INVENTORY_TIMEOUT_MS }) }) {
|
|
758
|
+
let parkFallback;
|
|
898
759
|
try {
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
id: udid
|
|
903
|
-
}, () => Boolean(resolveOwnedIosSim(udid).sim));
|
|
904
|
-
deleteSim(udid);
|
|
905
|
-
});
|
|
906
|
-
if (!removed) return {
|
|
760
|
+
if (del || wipe) await withConfigLockAsync$1(() => assertAvdReferences(avdName, owner));
|
|
761
|
+
const resolved = await resolveAvd(avdName);
|
|
762
|
+
if (resolved.notOwned) return {
|
|
907
763
|
status: "skipped",
|
|
908
|
-
kind: "not-
|
|
909
|
-
reason:
|
|
910
|
-
};
|
|
911
|
-
return {
|
|
912
|
-
status: "torn-down",
|
|
913
|
-
label: label ?? removed.name
|
|
914
|
-
};
|
|
915
|
-
} catch (error) {
|
|
916
|
-
return {
|
|
917
|
-
status: "failed",
|
|
918
|
-
reason: String(error?.message || error)
|
|
764
|
+
kind: "not-owned",
|
|
765
|
+
reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
|
|
919
766
|
};
|
|
920
|
-
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
closeOwnedDeviceSessions({
|
|
926
|
-
platform: "ios",
|
|
927
|
-
id: udid
|
|
928
|
-
}, () => Boolean(resolveOwnedIosSim(udid).sim));
|
|
929
|
-
const resolved = resolveOwnedIosSim(udid);
|
|
930
|
-
if (resolved.missing) throw new Error(`simulator ${udid} is not on this machine`);
|
|
931
|
-
if (resolved.notOwned) throw new Error(`simulator ${udid} is now named ${JSON.stringify(resolved.notOwned)}`);
|
|
932
|
-
const state = resolved.sim.state;
|
|
933
|
-
if (state !== "Shutdown") throw new Error(`simulator ${udid} is ${state}, not Shutdown; it was kept`);
|
|
934
|
-
eraseStarted();
|
|
935
|
-
eraseIosSim(udid);
|
|
936
|
-
});
|
|
937
|
-
if (!erased) return {
|
|
767
|
+
if (resolved.missing) {
|
|
768
|
+
if (wipe) throw new Error(`AVD ${avdName} is not registered; its data was kept.`);
|
|
769
|
+
return teardownUnregisteredAvd(avdName, del, owner, orphanedDirectory);
|
|
770
|
+
}
|
|
771
|
+
if (onlyIfMissing) return {
|
|
938
772
|
status: "skipped",
|
|
939
|
-
|
|
940
|
-
reason: "simulator is no longer parked"
|
|
773
|
+
reason: "AVD registration appeared; its record was kept."
|
|
941
774
|
};
|
|
942
|
-
return {
|
|
943
|
-
status: "
|
|
944
|
-
|
|
775
|
+
if (orphanedDirectory) return {
|
|
776
|
+
status: "skipped",
|
|
777
|
+
reason: "AVD registration appeared; its data was kept."
|
|
945
778
|
};
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
};
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
function eraseParkedAvd(avdName) {
|
|
954
|
-
try {
|
|
955
|
-
return eraseParkedAfter("android", avdName, (_record, eraseStarted) => {
|
|
956
|
-
if (resolveOwnedAvdSerial(avdName, { timeoutMs: AVD_INVENTORY_TIMEOUT_MS }).serial) throw new Error(`AVD ${avdName} is running; it was kept`);
|
|
957
|
-
const result = teardownOwnedAvd(avdName, {
|
|
958
|
-
wipe: true,
|
|
959
|
-
owner: { pool: true },
|
|
960
|
-
onWipeStart: eraseStarted
|
|
961
|
-
});
|
|
962
|
-
if (result.status !== "torn-down") throw new Error(result.reason ?? `AVD ${avdName} is ${result.status}`);
|
|
963
|
-
}) ? {
|
|
964
|
-
status: "torn-down",
|
|
965
|
-
label: avdName
|
|
966
|
-
} : {
|
|
967
|
-
status: "skipped",
|
|
968
|
-
kind: "not-parked",
|
|
969
|
-
reason: "emulator is no longer parked"
|
|
970
|
-
};
|
|
971
|
-
} catch (error) {
|
|
972
|
-
return {
|
|
973
|
-
status: "failed",
|
|
974
|
-
reason: String(error?.message || error)
|
|
975
|
-
};
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
const IOS_SHUTDOWN_SETTLE_MS = 15e3;
|
|
979
|
-
const ADB_RELEASE_WAIT_MS = 3e3;
|
|
980
|
-
const AVD_INVENTORY_TIMEOUT_MS = 15e3;
|
|
981
|
-
function repoRootAbove(workspace) {
|
|
982
|
-
if (workspace === void 0 || !getExecutor().findExecutable("agent-device")) return void 0;
|
|
983
|
-
const root = repoRoot(workspace);
|
|
984
|
-
try {
|
|
985
|
-
return root && realpathSync(root) !== realpathSync(workspace) ? root : void 0;
|
|
986
|
-
} catch {
|
|
987
|
-
return;
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
const IOS_SHUTDOWN_POLL_MS = 250;
|
|
991
|
-
const IOS_SHUTDOWN_ATTEMPTS = 2;
|
|
992
|
-
function settleIosSimShutdown(udid, { now = Date.now, sleep = sleepSync }) {
|
|
993
|
-
let resolved = resolveOwnedIosSim(udid);
|
|
994
|
-
for (let attempt = 1;; attempt++) {
|
|
995
|
-
const deadline = now() + IOS_SHUTDOWN_SETTLE_MS;
|
|
996
|
-
while (resolved.sim && resolved.sim.state !== "Shutdown" && now() < deadline) {
|
|
997
|
-
sleep(IOS_SHUTDOWN_POLL_MS);
|
|
998
|
-
resolved = resolveOwnedIosSim(udid);
|
|
999
|
-
}
|
|
1000
|
-
if (!resolved.sim || resolved.sim.state === "Shutdown" || attempt === IOS_SHUTDOWN_ATTEMPTS) return resolved;
|
|
1001
|
-
shutdownIosSim(udid);
|
|
1002
|
-
resolved = resolveOwnedIosSim(udid);
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
function iosShutdownFailureReason(udid, state) {
|
|
1006
|
-
return `simulator ${udid} is still ${state} after ${IOS_SHUTDOWN_ATTEMPTS} shutdown attempts and ${IOS_SHUTDOWN_ATTEMPTS * IOS_SHUTDOWN_SETTLE_MS / 1e3}s of waiting`;
|
|
1007
|
-
}
|
|
1008
|
-
function parkOwnedIosSim(settled, udid, park) {
|
|
1009
|
-
if (settled.missing) throw new Error(`simulator ${udid} disappeared after shutdown`);
|
|
1010
|
-
if (settled.notOwned) throw new Error(`simulator ${udid} is now named ${JSON.stringify(settled.notOwned)}`);
|
|
1011
|
-
const sim = settled.sim;
|
|
1012
|
-
if (sim.state !== "Shutdown") throw new Error(iosShutdownFailureReason(udid, sim.state));
|
|
1013
|
-
const model = listIosDeviceTypes().find((d) => d.identifier === sim.deviceTypeIdentifier)?.name ?? null;
|
|
1014
|
-
const runtime = parseRuntimeVersion(sim.runtime);
|
|
1015
|
-
const name = parkedSimName(sim.udid, {
|
|
1016
|
-
model,
|
|
1017
|
-
runtime
|
|
1018
|
-
});
|
|
1019
|
-
renameIosSim(sim.udid, name);
|
|
1020
|
-
const record = {
|
|
1021
|
-
udid: sim.udid,
|
|
1022
|
-
name,
|
|
1023
|
-
deviceTypeIdentifier: sim.deviceTypeIdentifier,
|
|
1024
|
-
runtimeIdentifier: sim.runtime,
|
|
1025
|
-
parkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1026
|
-
simslimManaged: Boolean(park.simslimManaged),
|
|
1027
|
-
...park.appId ? { bundleId: park.appId } : {},
|
|
1028
|
-
...park.cacheKey ? { cacheKey: park.cacheKey } : {}
|
|
1029
|
-
};
|
|
1030
|
-
return {
|
|
1031
|
-
record,
|
|
1032
|
-
evicted: parkSim({
|
|
1033
|
-
platform: "ios",
|
|
1034
|
-
projectPath: park.projectPath,
|
|
1035
|
-
slot: park.slot,
|
|
1036
|
-
record,
|
|
1037
|
-
max: park.max
|
|
1038
|
-
})
|
|
1039
|
-
};
|
|
1040
|
-
}
|
|
1041
|
-
function teardownOwnedIosSim(udid, { del = false, label, park, workspace, shutdownClock = {} } = {}) {
|
|
1042
|
-
let parkFallback;
|
|
1043
|
-
try {
|
|
1044
|
-
const resolved = resolveOwnedIosSim(udid);
|
|
1045
|
-
if (resolved.notOwned) return {
|
|
1046
|
-
status: "skipped",
|
|
1047
|
-
kind: "not-owned",
|
|
1048
|
-
reason: `sim "${resolved.notOwned}" is not Stim-owned: Stim has no record of creating it`
|
|
1049
|
-
};
|
|
1050
|
-
if (resolved.missing) return { status: "missing" };
|
|
1051
|
-
closeOwnedDeviceSessions({
|
|
1052
|
-
platform: "ios",
|
|
1053
|
-
id: udid
|
|
1054
|
-
}, () => Boolean(resolveOwnedIosSim(udid).sim), workspace);
|
|
1055
|
-
const current = resolveOwnedIosSim(udid);
|
|
1056
|
-
if (current.missing) return { status: "missing" };
|
|
1057
|
-
if (current.notOwned) return {
|
|
1058
|
-
status: "skipped",
|
|
1059
|
-
kind: "not-owned",
|
|
1060
|
-
reason: "simulator ownership changed before shutdown"
|
|
1061
|
-
};
|
|
1062
|
-
shutdownIosSim(udid);
|
|
1063
|
-
const sim = resolved.sim;
|
|
1064
|
-
if (del && park && park.max > 0) try {
|
|
1065
|
-
const { record, evicted } = parkOwnedIosSim(settleIosSimShutdown(udid, shutdownClock), udid, park);
|
|
1066
|
-
const removed = [];
|
|
1067
|
-
const failures = [];
|
|
1068
|
-
for (const entry of evicted) {
|
|
1069
|
-
const result = teardownParkedIosSim(entry.udid, { label: entry.name });
|
|
1070
|
-
if (result.status === "torn-down") removed.push({
|
|
1071
|
-
udid: entry.udid,
|
|
1072
|
-
name: entry.name
|
|
1073
|
-
});
|
|
1074
|
-
else if (result.status === "failed") failures.push(`could not delete evicted ${entry.name} (${entry.udid}): ${result.reason}`);
|
|
1075
|
-
}
|
|
1076
|
-
return {
|
|
1077
|
-
status: "torn-down",
|
|
1078
|
-
label: label ?? sim.name ?? udid,
|
|
1079
|
-
parked: {
|
|
1080
|
-
udid: record.udid,
|
|
1081
|
-
name: record.name
|
|
1082
|
-
},
|
|
1083
|
-
evicted: removed,
|
|
1084
|
-
...failures.length ? { evictionFailures: failures } : {}
|
|
1085
|
-
};
|
|
1086
|
-
} catch (e) {
|
|
1087
|
-
parkFallback = String(e?.message || e);
|
|
1088
|
-
}
|
|
1089
|
-
if (del) {
|
|
1090
|
-
deleteIosSim(udid);
|
|
1091
|
-
return {
|
|
1092
|
-
status: "torn-down",
|
|
1093
|
-
label: label ?? sim.name ?? udid,
|
|
1094
|
-
...parkFallback ? { parkFallback } : {}
|
|
1095
|
-
};
|
|
1096
|
-
}
|
|
1097
|
-
const settled = settleIosSimShutdown(udid, shutdownClock);
|
|
1098
|
-
if (settled.sim && settled.sim.state !== "Shutdown") return {
|
|
1099
|
-
status: "failed",
|
|
1100
|
-
label: label ?? sim.name ?? udid,
|
|
1101
|
-
reason: iosShutdownFailureReason(udid, settled.sim.state)
|
|
1102
|
-
};
|
|
1103
|
-
const root = sim.state === "Shutdown" ? void 0 : repoRootAbove(workspace);
|
|
1104
|
-
if (root && settled.sim) closeOwnedDeviceSessions({
|
|
1105
|
-
platform: "ios",
|
|
1106
|
-
id: udid
|
|
1107
|
-
}, () => resolveOwnedIosSim(udid).sim?.state === "Shutdown", workspace, root);
|
|
1108
|
-
return {
|
|
1109
|
-
status: "torn-down",
|
|
1110
|
-
label: label ?? sim.name ?? udid
|
|
1111
|
-
};
|
|
1112
|
-
} catch (e) {
|
|
1113
|
-
return {
|
|
1114
|
-
status: "failed",
|
|
1115
|
-
reason: String(e?.message || e),
|
|
1116
|
-
...parkFallback ? { parkFallback } : {}
|
|
1117
|
-
};
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
function assertAvdReferences(avdName, owner) {
|
|
1121
|
-
const config = loadConfig$1();
|
|
1122
|
-
if (!config) throw new Error("Cannot verify AVD references without a Stim config; the data was kept.");
|
|
1123
|
-
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.`);
|
|
1124
|
-
for (const [projectPath, project] of Object.entries(config.projects)) for (const { slot, platforms } of projectDeviceSlots(project)) {
|
|
1125
|
-
if (platforms.android?.avdName?.toLowerCase() !== avdName.toLowerCase()) continue;
|
|
1126
|
-
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.`);
|
|
1127
|
-
}
|
|
1128
|
-
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.`);
|
|
1129
|
-
}
|
|
1130
|
-
function removeOrphanedAvdDirectory(candidate, owner) {
|
|
1131
|
-
const surveyed = listOrphanedAvdDirectories(candidate.name).find((entry) => entry.directory === candidate.directory);
|
|
1132
|
-
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.`);
|
|
1133
|
-
assertOwnedAvdStopped(candidate.name, { resolveDirectory: () => candidate.directory });
|
|
1134
|
-
const detached = withConfigLock$1(() => {
|
|
1135
|
-
const current = lstatSync(candidate.directory);
|
|
1136
|
-
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.`);
|
|
1137
|
-
assertAvdReferences(candidate.name, owner);
|
|
1138
|
-
const name = `stim-gc-${randomUUID()}`;
|
|
1139
|
-
recordCreatedDevice("android", name);
|
|
1140
|
-
const destination = join(dirname(candidate.directory), `${name}.avd`);
|
|
1141
|
-
try {
|
|
1142
|
-
renameSync(candidate.directory, destination);
|
|
1143
|
-
} catch (error) {
|
|
1144
|
-
forgetCreatedDevice("android", name);
|
|
1145
|
-
throw error;
|
|
1146
|
-
}
|
|
1147
|
-
forgetCreatedDevice("android", candidate.name);
|
|
1148
|
-
return destination;
|
|
1149
|
-
});
|
|
1150
|
-
const current = lstatSync(detached);
|
|
1151
|
-
if (!current.isDirectory() || current.dev !== candidate.dev || current.ino !== candidate.ino) throw new Error(`Detached AVD data changed at ${detached}; it was kept.`);
|
|
1152
|
-
try {
|
|
1153
|
-
rmSync(detached, { recursive: true });
|
|
1154
|
-
} catch (error) {
|
|
1155
|
-
throw new Error(`Could not remove AVD data at ${detached}; retry gc --delete: ${error.message}`, { cause: error });
|
|
1156
|
-
}
|
|
1157
|
-
forgetCreatedDevice("android", basename(detached, ".avd"));
|
|
1158
|
-
}
|
|
1159
|
-
function teardownUnregisteredAvd(avdName, del, owner, surveyed) {
|
|
1160
|
-
const candidates = surveyed ? [surveyed] : listOrphanedAvdDirectories(avdName);
|
|
1161
|
-
if (candidates.length === 0) {
|
|
1162
|
-
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.`);
|
|
1163
|
-
return { status: "missing" };
|
|
1164
|
-
}
|
|
1165
|
-
if (!del) return {
|
|
1166
|
-
status: "skipped",
|
|
1167
|
-
reason: "AVD registration is missing; its data was kept."
|
|
1168
|
-
};
|
|
1169
|
-
for (const candidate of candidates) {
|
|
1170
|
-
if (candidate.name !== avdName) throw new Error(`AVD data does not match ${avdName}; it was kept.`);
|
|
1171
|
-
removeOrphanedAvdDirectory(candidate, owner);
|
|
1172
|
-
}
|
|
1173
|
-
return {
|
|
1174
|
-
status: "torn-down",
|
|
1175
|
-
label: avdName
|
|
1176
|
-
};
|
|
1177
|
-
}
|
|
1178
|
-
function teardownOwnedAvd(avdName, options = {}) {
|
|
1179
|
-
let claim;
|
|
1180
|
-
try {
|
|
1181
|
-
if (!isStimOwnedAvd(avdName)) return {
|
|
1182
|
-
status: "skipped",
|
|
1183
|
-
kind: "not-owned",
|
|
1184
|
-
reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
|
|
1185
|
-
};
|
|
1186
|
-
claim = acquireAvdClaim(avdName);
|
|
1187
|
-
const result = teardownClaimedAvd(avdName, claim, options);
|
|
1188
|
-
if (result.status === "torn-down" || result.status === "missing") options.onRemoved?.();
|
|
1189
|
-
return result;
|
|
1190
|
-
} catch (error) {
|
|
1191
|
-
return {
|
|
1192
|
-
status: "failed",
|
|
1193
|
-
reason: String(error?.message || error)
|
|
1194
|
-
};
|
|
1195
|
-
} finally {
|
|
1196
|
-
(0, ownership_claim_exports.releaseClaim)(claim);
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
function serialFree(serial, timeoutMs = AVD_INVENTORY_TIMEOUT_MS) {
|
|
1200
|
-
const adb = listAdbDevices({ timeoutMs });
|
|
1201
|
-
return ![
|
|
1202
|
-
...adb.emulators,
|
|
1203
|
-
...adb.physical,
|
|
1204
|
-
...adb.unhealthy
|
|
1205
|
-
].some((entry) => entry.serial === serial);
|
|
1206
|
-
}
|
|
1207
|
-
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 }) }) {
|
|
1208
|
-
let parkFallback;
|
|
1209
|
-
try {
|
|
1210
|
-
if (del || wipe) withConfigLock$1(() => assertAvdReferences(avdName, owner));
|
|
1211
|
-
const resolved = resolveAvd(avdName);
|
|
1212
|
-
if (resolved.notOwned) return {
|
|
1213
|
-
status: "skipped",
|
|
1214
|
-
kind: "not-owned",
|
|
1215
|
-
reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
|
|
1216
|
-
};
|
|
1217
|
-
if (resolved.missing) {
|
|
1218
|
-
if (wipe) throw new Error(`AVD ${avdName} is not registered; its data was kept.`);
|
|
1219
|
-
return teardownUnregisteredAvd(avdName, del, owner, orphanedDirectory);
|
|
1220
|
-
}
|
|
1221
|
-
if (onlyIfMissing) return {
|
|
1222
|
-
status: "skipped",
|
|
1223
|
-
reason: "AVD registration appeared; its record was kept."
|
|
1224
|
-
};
|
|
1225
|
-
if (orphanedDirectory) return {
|
|
1226
|
-
status: "skipped",
|
|
1227
|
-
reason: "AVD registration appeared; its data was kept."
|
|
1228
|
-
};
|
|
1229
|
-
const serial = resolved.serial;
|
|
1230
|
-
const stillStopped = (sessionSerial) => {
|
|
1231
|
-
const current = resolveAvd(avdName);
|
|
1232
|
-
return !current.notOwned && !current.missing && !current.serial && serialFree(sessionSerial);
|
|
779
|
+
const serial = resolved.serial;
|
|
780
|
+
const stillStopped = async (sessionSerial) => {
|
|
781
|
+
const current = await resolveAvd(avdName);
|
|
782
|
+
return !current.notOwned && !current.missing && !current.serial && await serialFree(sessionSerial);
|
|
1233
783
|
};
|
|
1234
784
|
if (serial) {
|
|
1235
|
-
const stillOwned = () => {
|
|
1236
|
-
const current = resolveAvd(avdName);
|
|
785
|
+
const stillOwned = async () => {
|
|
786
|
+
const current = await resolveAvd(avdName);
|
|
1237
787
|
return !current.notOwned && !current.missing && current.serial === serial;
|
|
1238
788
|
};
|
|
1239
|
-
closeOwnedDeviceSessions({
|
|
789
|
+
await closeOwnedDeviceSessions({
|
|
1240
790
|
platform: "android",
|
|
1241
791
|
id: serial,
|
|
1242
792
|
avdName
|
|
1243
793
|
}, stillOwned, workspace);
|
|
1244
|
-
if (!stillOwned()) throw new Error(`Owned AVD ${avdName} changed before shutdown; it was kept.`);
|
|
1245
|
-
waitForShutdown(avdName, (timeoutMs) => {
|
|
794
|
+
if (!await stillOwned()) throw new Error(`Owned AVD ${avdName} changed before shutdown; it was kept.`);
|
|
795
|
+
await waitForShutdown(avdName, async (timeoutMs) => {
|
|
1246
796
|
(0, ownership_claim_exports.markClaimChildPending)(claim);
|
|
1247
797
|
try {
|
|
1248
|
-
shutdownAndroidEmulator(serial, timeoutMs);
|
|
798
|
+
await shutdownAndroidEmulator(serial, timeoutMs);
|
|
1249
799
|
} finally {
|
|
1250
800
|
(0, ownership_claim_exports.clearClaimChild)(claim);
|
|
1251
801
|
}
|
|
1252
802
|
});
|
|
1253
|
-
const root = repoRootAbove(workspace);
|
|
803
|
+
const root = await repoRootAbove(workspace);
|
|
1254
804
|
if (root) {
|
|
1255
805
|
const deadline = Date.now() + ADB_RELEASE_WAIT_MS;
|
|
1256
|
-
while (!serialFree(serial, Math.max(1, deadline - Date.now())) && Date.now() < deadline)
|
|
1257
|
-
closeOwnedDeviceSessions({
|
|
806
|
+
while (!await serialFree(serial, Math.max(1, deadline - Date.now())) && Date.now() < deadline) await sleepAsync(250);
|
|
807
|
+
await closeOwnedDeviceSessions({
|
|
1258
808
|
platform: "android",
|
|
1259
809
|
id: serial,
|
|
1260
810
|
avdName
|
|
1261
811
|
}, stillStopped, workspace, root);
|
|
1262
812
|
}
|
|
1263
813
|
} else {
|
|
1264
|
-
closeOwnedDeviceSessions({
|
|
814
|
+
await closeOwnedDeviceSessions({
|
|
1265
815
|
platform: "android",
|
|
1266
816
|
id: null,
|
|
1267
817
|
avdName
|
|
1268
818
|
}, stillStopped, workspace);
|
|
1269
|
-
waitForShutdown(avdName, null);
|
|
819
|
+
await waitForShutdown(avdName, null);
|
|
1270
820
|
}
|
|
1271
821
|
if (wipe && !del) {
|
|
1272
|
-
const current = resolveAvd(avdName);
|
|
822
|
+
const current = await resolveAvd(avdName);
|
|
1273
823
|
if (current.notOwned || current.missing) throw new Error(`AVD ${avdName} changed before its data was wiped.`);
|
|
1274
824
|
if (current.serial) throw new Error(`Owned AVD ${avdName} started again before its data was wiped.`);
|
|
1275
825
|
assertStopped(avdName);
|
|
1276
|
-
|
|
826
|
+
await withConfigLockAsync$1(() => assertAvdReferences(avdName, owner));
|
|
1277
827
|
const directory = ownedAvdDirectory(avdName);
|
|
1278
828
|
if (!directory) throw new Error(`The data directory of AVD ${avdName} could not be resolved.`);
|
|
1279
829
|
onWipeStart?.();
|
|
1280
830
|
wipeAvdUserData(directory);
|
|
1281
831
|
}
|
|
1282
832
|
if (del) {
|
|
1283
|
-
const current = resolveAvd(avdName);
|
|
833
|
+
const current = await resolveAvd(avdName);
|
|
1284
834
|
if (current.notOwned) return {
|
|
1285
835
|
status: "skipped",
|
|
1286
836
|
kind: "not-owned",
|
|
@@ -1289,7 +839,7 @@ function teardownClaimedAvd(avdName, claim, { del = false, wipe = false, park, o
|
|
|
1289
839
|
if (current.missing) return teardownUnregisteredAvd(avdName, del, owner);
|
|
1290
840
|
if (current.serial) throw new Error(`Owned AVD ${avdName} started again before deletion.`);
|
|
1291
841
|
assertStopped(avdName);
|
|
1292
|
-
|
|
842
|
+
await withConfigLockAsync$1(() => assertAvdReferences(avdName, owner));
|
|
1293
843
|
if (park && park.max > 0) try {
|
|
1294
844
|
const systemImage = ownedAvdSystemImage(avdName);
|
|
1295
845
|
if (!systemImage || !park.configuration || !ownedAvdMatchesConfiguration(avdName, park.configuration)) throw new Error("the AVD has no verified creation configuration");
|
|
@@ -1311,7 +861,7 @@ function teardownClaimedAvd(avdName, claim, { del = false, wipe = false, park, o
|
|
|
1311
861
|
const removed = [];
|
|
1312
862
|
const failures = [];
|
|
1313
863
|
for (const entry of evicted) {
|
|
1314
|
-
const result = teardownParkedAvd(entry.name);
|
|
864
|
+
const result = await teardownParkedAvd(entry.name);
|
|
1315
865
|
if (result.status === "torn-down") removed.push({
|
|
1316
866
|
udid: entry.name,
|
|
1317
867
|
name: entry.name,
|
|
@@ -1338,7 +888,7 @@ function teardownClaimedAvd(avdName, claim, { del = false, wipe = false, park, o
|
|
|
1338
888
|
if (directory) paths.push(directory);
|
|
1339
889
|
(0, ownership_claim_exports.markClaimChildPending)(claim);
|
|
1340
890
|
try {
|
|
1341
|
-
deleteAvd(avdName);
|
|
891
|
+
await deleteAvd(avdName);
|
|
1342
892
|
} finally {
|
|
1343
893
|
(0, ownership_claim_exports.clearClaimChild)(claim);
|
|
1344
894
|
}
|
|
@@ -1362,16 +912,16 @@ const BROWSER_EXIT_WAIT_MS = 15e3;
|
|
|
1362
912
|
const CHROME_KILL_WAIT_MS = 5e3;
|
|
1363
913
|
async function stopOwnedChrome(record, profile) {
|
|
1364
914
|
for (const signal of ["SIGTERM", "SIGKILL"]) {
|
|
1365
|
-
const state =
|
|
915
|
+
const state = await chromeProcessStateAsync(record, profile);
|
|
1366
916
|
if (state === "gone") return true;
|
|
1367
917
|
if (state === "unknown") return false;
|
|
1368
918
|
try {
|
|
1369
|
-
|
|
919
|
+
await signalProcessTreeAsync(record.pid, signal, { group: true });
|
|
1370
920
|
} catch {}
|
|
1371
921
|
const deadline = Date.now() + CHROME_KILL_WAIT_MS;
|
|
1372
|
-
while (
|
|
922
|
+
while (await chromeProcessStateAsync(record, profile) !== "gone" && Date.now() < deadline) await sleepAsync(50);
|
|
1373
923
|
}
|
|
1374
|
-
return
|
|
924
|
+
return await chromeProcessStateAsync(record, profile) === "gone";
|
|
1375
925
|
}
|
|
1376
926
|
const sleepAsync = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
1377
927
|
/**
|
|
@@ -1433,7 +983,7 @@ async function teardownBrowserHeld(root, { deleteProfile = false } = {}) {
|
|
|
1433
983
|
kind: "not-verified",
|
|
1434
984
|
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))}`
|
|
1435
985
|
};
|
|
1436
|
-
clearWebRecord(root, record);
|
|
986
|
+
await withWorkspaceStateLockAsync(root, () => clearWebRecord(root, record));
|
|
1437
987
|
await releaseBrowserPort(root, record.cdpPort);
|
|
1438
988
|
} else {
|
|
1439
989
|
const holder = liveProfileHolder(profile);
|
|
@@ -1469,82 +1019,620 @@ async function teardownBrowserHeld(root, { deleteProfile = false } = {}) {
|
|
|
1469
1019
|
};
|
|
1470
1020
|
}
|
|
1471
1021
|
}
|
|
1472
|
-
//#endregion
|
|
1473
|
-
//#region src/macos/instances.ts
|
|
1474
|
-
const BUNDLE_EXECUTABLE = /^[^/]+\.app\/Contents\/MacOS\/[^/]+$/;
|
|
1475
|
-
function bundleDirs(root) {
|
|
1476
|
-
const dir = macosDir(root);
|
|
1477
|
-
if (process.platform !== "darwin" || !existsSync(dir)) return [];
|
|
1478
|
-
return [.../* @__PURE__ */ new Set([dir, realpathSync(dir)])];
|
|
1022
|
+
//#endregion
|
|
1023
|
+
//#region src/macos/instances.ts
|
|
1024
|
+
const BUNDLE_EXECUTABLE = /^[^/]+\.app\/Contents\/MacOS\/[^/]+$/;
|
|
1025
|
+
function bundleDirs(root) {
|
|
1026
|
+
const dir = macosDir(root);
|
|
1027
|
+
if (process.platform !== "darwin" || !existsSync(dir)) return [];
|
|
1028
|
+
return [.../* @__PURE__ */ new Set([dir, realpathSync(dir)])];
|
|
1029
|
+
}
|
|
1030
|
+
function isBundleExecutable(path, dirs) {
|
|
1031
|
+
return dirs.some((dir) => path.startsWith(`${dir}/`) && BUNDLE_EXECUTABLE.test(path.slice(dir.length + 1)));
|
|
1032
|
+
}
|
|
1033
|
+
/** Pids from `ps -axww -o pid=,comm=` output whose command names a bundle executable inside `dirs`. */
|
|
1034
|
+
function bundleCandidates(psOutput, dirs) {
|
|
1035
|
+
const pids = [];
|
|
1036
|
+
for (const line of psOutput.split("\n")) {
|
|
1037
|
+
const match = /^\s*(\d+)\s+(.+)$/.exec(line);
|
|
1038
|
+
if (match && isBundleExecutable(match[2], dirs)) pids.push(Number(match[1]));
|
|
1039
|
+
}
|
|
1040
|
+
return pids;
|
|
1041
|
+
}
|
|
1042
|
+
/** Whether `lsof -Fn -d txt` output for one process names a bundle executable inside `dirs`. */
|
|
1043
|
+
function lsofNamesBundleExecutable(lsofOutput, dirs) {
|
|
1044
|
+
return lsofOutput.split("\n").some((line) => line.startsWith("n") && isBundleExecutable(line.slice(1), dirs));
|
|
1045
|
+
}
|
|
1046
|
+
function candidatePids(dirs) {
|
|
1047
|
+
if (!dirs.length) return [];
|
|
1048
|
+
return bundleCandidates(getExecutor().runFile("ps", [
|
|
1049
|
+
"-axww",
|
|
1050
|
+
"-o",
|
|
1051
|
+
"pid=,comm="
|
|
1052
|
+
], { timeoutMs: 1e4 }), dirs).filter((pid) => pid !== process.pid);
|
|
1053
|
+
}
|
|
1054
|
+
/** Pids that claim to run an app bundle from the workspace's macOS directory. `ps` reports argv[0], so this is a lead. */
|
|
1055
|
+
function bundleInstancePids(root) {
|
|
1056
|
+
return candidatePids(bundleDirs(root));
|
|
1057
|
+
}
|
|
1058
|
+
function unverified(pid, why) {
|
|
1059
|
+
return Object.assign(/* @__PURE__ */ new Error(`Cannot verify macOS app pid ${pid}: ${why}; no signal was sent.`), { code: "STIM_MACOS_OWNER_UNVERIFIED" });
|
|
1060
|
+
}
|
|
1061
|
+
function bundleRecord(pid) {
|
|
1062
|
+
const processToken = (0, process_identity_exports.captureProcessToken)(pid);
|
|
1063
|
+
const start = (0, process_identity_exports.processStartMicros)(pid);
|
|
1064
|
+
if (!processToken || start.status !== "running") {
|
|
1065
|
+
if (start.status === "gone") return null;
|
|
1066
|
+
throw unverified(pid, "its identity cannot be captured");
|
|
1067
|
+
}
|
|
1068
|
+
return {
|
|
1069
|
+
pid,
|
|
1070
|
+
processToken,
|
|
1071
|
+
startedAtMicros: start.startedAtMicros
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
function failedBundleListing(record, error) {
|
|
1075
|
+
if (["gone", "different"].includes((0, process_identity_exports.inspectProcessIdentity)(record))) return null;
|
|
1076
|
+
const open = String(error.stdout ?? "");
|
|
1077
|
+
if (!open.trim()) throw unverified(record.pid, `lsof failed (${error.message.split("\n")[0]})`);
|
|
1078
|
+
return open;
|
|
1079
|
+
}
|
|
1080
|
+
/** Verifies owned bundle instances without blocking on process and executable probes. */
|
|
1081
|
+
async function runningBundleInstancesAsync(root) {
|
|
1082
|
+
const dirs = bundleDirs(root);
|
|
1083
|
+
if (!dirs.length) return [];
|
|
1084
|
+
const listing = await getExecutor().runFileAsync("ps", [
|
|
1085
|
+
"-axww",
|
|
1086
|
+
"-o",
|
|
1087
|
+
"pid=,comm="
|
|
1088
|
+
], {
|
|
1089
|
+
timeoutMs: 1e4,
|
|
1090
|
+
killSignal: "SIGKILL"
|
|
1091
|
+
});
|
|
1092
|
+
const instances = [];
|
|
1093
|
+
for (const pid of bundleCandidates(listing, dirs).filter((candidate) => candidate !== process.pid)) {
|
|
1094
|
+
const record = bundleRecord(pid);
|
|
1095
|
+
if (!record) continue;
|
|
1096
|
+
let open;
|
|
1097
|
+
try {
|
|
1098
|
+
open = await getExecutor().runFileAsync("lsof", [
|
|
1099
|
+
"-a",
|
|
1100
|
+
"-p",
|
|
1101
|
+
String(pid),
|
|
1102
|
+
"-d",
|
|
1103
|
+
"txt",
|
|
1104
|
+
"-Fn"
|
|
1105
|
+
], {
|
|
1106
|
+
timeoutMs: 1e4,
|
|
1107
|
+
killSignal: "SIGKILL"
|
|
1108
|
+
});
|
|
1109
|
+
} catch (error) {
|
|
1110
|
+
const partial = failedBundleListing(record, error);
|
|
1111
|
+
if (partial === null) continue;
|
|
1112
|
+
open = partial;
|
|
1113
|
+
}
|
|
1114
|
+
if (lsofNamesBundleExecutable(open, dirs)) instances.push(record);
|
|
1115
|
+
}
|
|
1116
|
+
return instances;
|
|
1117
|
+
}
|
|
1118
|
+
//#endregion
|
|
1119
|
+
//#region src/engine/tunnel.ts
|
|
1120
|
+
function tunnelArgv(provider, port, ngrokUrl, logFile, tailscaleBinary = "tailscale") {
|
|
1121
|
+
if (provider === "tailscale") return {
|
|
1122
|
+
bin: tailscaleBinary,
|
|
1123
|
+
args: [
|
|
1124
|
+
"serve",
|
|
1125
|
+
`--https=${port}`,
|
|
1126
|
+
`http://127.0.0.1:${port}`
|
|
1127
|
+
]
|
|
1128
|
+
};
|
|
1129
|
+
if (provider === "cloudflared") return {
|
|
1130
|
+
bin: "cloudflared",
|
|
1131
|
+
args: [
|
|
1132
|
+
"tunnel",
|
|
1133
|
+
"--url",
|
|
1134
|
+
`http://127.0.0.1:${port}`,
|
|
1135
|
+
...logFile ? ["--logfile", logFile] : []
|
|
1136
|
+
]
|
|
1137
|
+
};
|
|
1138
|
+
return {
|
|
1139
|
+
bin: "ngrok",
|
|
1140
|
+
args: [
|
|
1141
|
+
"http",
|
|
1142
|
+
String(port),
|
|
1143
|
+
`--log=${logFile || "stdout"}`,
|
|
1144
|
+
"--log-format=json",
|
|
1145
|
+
...ngrokUrl ? ["--url", ngrokUrl] : []
|
|
1146
|
+
]
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
const CLOUDFLARED_URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/i;
|
|
1150
|
+
function parseCloudflaredLine(line) {
|
|
1151
|
+
const match = line.match(CLOUDFLARED_URL_RE);
|
|
1152
|
+
return match ? match[0] : null;
|
|
1153
|
+
}
|
|
1154
|
+
function parseNgrokLine(line) {
|
|
1155
|
+
let data;
|
|
1156
|
+
try {
|
|
1157
|
+
data = JSON.parse(line);
|
|
1158
|
+
} catch {
|
|
1159
|
+
return null;
|
|
1160
|
+
}
|
|
1161
|
+
if (typeof data !== "object" || data === null) return null;
|
|
1162
|
+
const url = data.url;
|
|
1163
|
+
return typeof url === "string" && /^https?:\/\//i.test(url) ? url : null;
|
|
1164
|
+
}
|
|
1165
|
+
function parseTailscaleLine(line) {
|
|
1166
|
+
return /^https:\/\/[^\s/]+\.ts\.net(:\d+)?\/?$/.test(line) ? line : null;
|
|
1167
|
+
}
|
|
1168
|
+
function parserFor(provider) {
|
|
1169
|
+
if (provider === "tailscale") return parseTailscaleLine;
|
|
1170
|
+
return provider === "cloudflared" ? parseCloudflaredLine : parseNgrokLine;
|
|
1171
|
+
}
|
|
1172
|
+
const URL_TIMEOUT_MS = 15e3;
|
|
1173
|
+
const REACHABLE_TIMEOUT_MS = 24e4;
|
|
1174
|
+
const REACHABLE_POLL_MS = 2e3;
|
|
1175
|
+
const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1176
|
+
async function withManagedTunnelLock(root, fn, options = {}) {
|
|
1177
|
+
return withWorkspaceProcessLock(managedTunnelLockRoot(root), "metro-tunnel", fn, {
|
|
1178
|
+
...options,
|
|
1179
|
+
rejectOwnerPurposes: ["workspace removal"]
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
async function withManagedTunnelRemovalLock(root, fn, options = {}) {
|
|
1183
|
+
return withWorkspaceProcessLock(managedTunnelLockRoot(root), "metro-tunnel", fn, {
|
|
1184
|
+
...options,
|
|
1185
|
+
ownerPurpose: "workspace removal"
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
async function withManagedRemoteWorktreeLock(worktreeRoot, fn, options = {}) {
|
|
1189
|
+
return withWorkspaceProcessLock(managedRemoteWorktreeLockRoot(worktreeRoot), "managed-remote", fn, {
|
|
1190
|
+
...options,
|
|
1191
|
+
external: true,
|
|
1192
|
+
rejectOwnerPurposes: ["worktree removal"]
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
async function withManagedRemoteWorktreeRemovalLock(worktreeRoot, fn, options = {}) {
|
|
1196
|
+
return withWorkspaceProcessLock(managedRemoteWorktreeLockRoot(worktreeRoot), "managed-remote", fn, {
|
|
1197
|
+
...options,
|
|
1198
|
+
external: true,
|
|
1199
|
+
ownerPurpose: "worktree removal"
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
function managedLockHolders(root) {
|
|
1203
|
+
const locks = [{
|
|
1204
|
+
label: "managed tunnel",
|
|
1205
|
+
path: workspaceProcessLockPath(managedTunnelLockRoot(root), "metro-tunnel", false)
|
|
1206
|
+
}, {
|
|
1207
|
+
label: "managed remote",
|
|
1208
|
+
path: workspaceProcessLockPath(managedRemoteWorktreeLockRoot(root), "managed-remote", true)
|
|
1209
|
+
}];
|
|
1210
|
+
const reasons = [];
|
|
1211
|
+
for (const { label, path } of locks) {
|
|
1212
|
+
const claims = (0, ownership_claim_exports.readClaimSet)(path);
|
|
1213
|
+
const holder = claims.live[0];
|
|
1214
|
+
if (holder) {
|
|
1215
|
+
const purpose = typeof holder.details.purpose === "string" ? ` for ${holder.details.purpose}` : "";
|
|
1216
|
+
reasons.push(`the ${label} lock is held${purpose}`);
|
|
1217
|
+
} else if (claims.unresolved[0]) reasons.push(`the ${label} lock cannot be resolved: ${claims.unresolved[0].reason}`);
|
|
1218
|
+
}
|
|
1219
|
+
return reasons;
|
|
1220
|
+
}
|
|
1221
|
+
function managedTunnelLockRoot(root) {
|
|
1222
|
+
const key = createHash("sha256").update(resolve(root)).digest("hex");
|
|
1223
|
+
return join(getConfigDir(), "process-locks", key);
|
|
1224
|
+
}
|
|
1225
|
+
function managedRemoteWorktreeLockRoot(worktreeRoot) {
|
|
1226
|
+
const key = createHash("sha256").update(resolve(worktreeRoot)).digest("hex");
|
|
1227
|
+
return join(getConfigDir(), "process-locks", "worktrees", key);
|
|
1228
|
+
}
|
|
1229
|
+
async function defaultProbeReachable(url, signal) {
|
|
1230
|
+
const status = await probePublicHttp(url, signal);
|
|
1231
|
+
return status !== null && status > 0;
|
|
1232
|
+
}
|
|
1233
|
+
function waitForUrl(child, parseLine, timeoutMs) {
|
|
1234
|
+
return new Promise((resolve) => {
|
|
1235
|
+
let settled = false;
|
|
1236
|
+
let lastLine = null;
|
|
1237
|
+
let timer;
|
|
1238
|
+
let onOut;
|
|
1239
|
+
let onErr;
|
|
1240
|
+
let onError;
|
|
1241
|
+
let onExit;
|
|
1242
|
+
const finish = (url, exited = false) => {
|
|
1243
|
+
if (settled) return;
|
|
1244
|
+
settled = true;
|
|
1245
|
+
clearTimeout(timer);
|
|
1246
|
+
child.stdout?.removeListener("data", onOut);
|
|
1247
|
+
child.stderr?.removeListener("data", onErr);
|
|
1248
|
+
child.removeListener("error", onError);
|
|
1249
|
+
child.removeListener("exit", onExit);
|
|
1250
|
+
resolve({
|
|
1251
|
+
url,
|
|
1252
|
+
exited,
|
|
1253
|
+
lastLine
|
|
1254
|
+
});
|
|
1255
|
+
};
|
|
1256
|
+
const onLine = (line) => {
|
|
1257
|
+
if (settled) return;
|
|
1258
|
+
if (line.trim()) lastLine = line.trim();
|
|
1259
|
+
const url = parseLine(line);
|
|
1260
|
+
if (url) finish(url);
|
|
1261
|
+
};
|
|
1262
|
+
const outReader = createLineReader(onLine);
|
|
1263
|
+
const errReader = createLineReader(onLine);
|
|
1264
|
+
onOut = (chunk) => outReader.push(chunk);
|
|
1265
|
+
onErr = (chunk) => errReader.push(chunk);
|
|
1266
|
+
onError = () => finish(null);
|
|
1267
|
+
onExit = () => {
|
|
1268
|
+
outReader.flush();
|
|
1269
|
+
errReader.flush();
|
|
1270
|
+
finish(null, true);
|
|
1271
|
+
};
|
|
1272
|
+
child.stdout?.setEncoding?.("utf-8");
|
|
1273
|
+
child.stderr?.setEncoding?.("utf-8");
|
|
1274
|
+
child.stdout?.on("data", onOut);
|
|
1275
|
+
child.stderr?.on("data", onErr);
|
|
1276
|
+
child.on("error", onError);
|
|
1277
|
+
child.on("exit", onExit);
|
|
1278
|
+
timer = setTimeout(() => finish(null), timeoutMs);
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
function waitForFileUrl(child, logFile, parseLine, timeoutMs) {
|
|
1282
|
+
return new Promise((resolve) => {
|
|
1283
|
+
let settled = false;
|
|
1284
|
+
let watcher = null;
|
|
1285
|
+
const finish = (url, exited = false) => {
|
|
1286
|
+
if (settled) return;
|
|
1287
|
+
settled = true;
|
|
1288
|
+
watcher?.close();
|
|
1289
|
+
clearInterval(interval);
|
|
1290
|
+
clearTimeout(timer);
|
|
1291
|
+
child.removeListener("error", onError);
|
|
1292
|
+
child.removeListener("exit", onExit);
|
|
1293
|
+
resolve({
|
|
1294
|
+
url,
|
|
1295
|
+
exited
|
|
1296
|
+
});
|
|
1297
|
+
};
|
|
1298
|
+
const probe = () => {
|
|
1299
|
+
try {
|
|
1300
|
+
for (const line of readFileSync(logFile, "utf-8").split(/\r?\n/)) {
|
|
1301
|
+
const url = parseLine(line);
|
|
1302
|
+
if (url) return finish(url);
|
|
1303
|
+
}
|
|
1304
|
+
} catch {}
|
|
1305
|
+
};
|
|
1306
|
+
const onError = () => finish(null);
|
|
1307
|
+
const onExit = () => finish(null, true);
|
|
1308
|
+
child.on("error", onError);
|
|
1309
|
+
child.on("exit", onExit);
|
|
1310
|
+
try {
|
|
1311
|
+
watcher = watch(dirname(logFile), (_event, name) => {
|
|
1312
|
+
if (name === null || name.toString() === basename(logFile)) probe();
|
|
1313
|
+
});
|
|
1314
|
+
watcher.on("error", () => {
|
|
1315
|
+
watcher?.close();
|
|
1316
|
+
watcher = null;
|
|
1317
|
+
});
|
|
1318
|
+
watcher.unref();
|
|
1319
|
+
} catch {}
|
|
1320
|
+
const interval = setInterval(probe, Math.min(1e3, timeoutMs));
|
|
1321
|
+
const timer = setTimeout(() => finish(null), timeoutMs);
|
|
1322
|
+
probe();
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
function createTunnelLogFile(provider) {
|
|
1326
|
+
const dir = join(getConfigDir(), "tunnel-logs");
|
|
1327
|
+
mkdirSync(dir, { recursive: true });
|
|
1328
|
+
const path = join(dir, `${provider}-${randomUUID()}.log`);
|
|
1329
|
+
closeSync(openSync(path, "wx", 384));
|
|
1330
|
+
return path;
|
|
1331
|
+
}
|
|
1332
|
+
function removeTunnelLogFile(path) {
|
|
1333
|
+
if (!path) return;
|
|
1334
|
+
try {
|
|
1335
|
+
unlinkSync(path);
|
|
1336
|
+
} catch {}
|
|
1337
|
+
}
|
|
1338
|
+
function removeRecordedTunnelLogFile(path) {
|
|
1339
|
+
if (!path) return;
|
|
1340
|
+
const dir = resolve(join(getConfigDir(), "tunnel-logs"));
|
|
1341
|
+
const resolved = resolve(path);
|
|
1342
|
+
if (!resolved.startsWith(`${dir}${sep}`) || resolve(join(dir, basename(resolved))) !== resolved) return;
|
|
1343
|
+
removeTunnelLogFile(resolved);
|
|
1344
|
+
}
|
|
1345
|
+
async function waitUntilReachable({ url, now, sleep, probe, timeoutMs }) {
|
|
1346
|
+
const deadline = now() + timeoutMs;
|
|
1347
|
+
for (;;) {
|
|
1348
|
+
if (await probe(url, new AbortController().signal).catch(() => false)) return true;
|
|
1349
|
+
if (now() >= deadline) return false;
|
|
1350
|
+
await sleep(REACHABLE_POLL_MS);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
const CLEANUP_TIMEOUT_MS = 1e3;
|
|
1354
|
+
const CLEANUP_POLL_MS = 25;
|
|
1355
|
+
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 }) {
|
|
1356
|
+
const spawn = spawnFn || ((cmd, args, opts) => getExecutor().spawn(cmd, args, opts));
|
|
1357
|
+
const outputFile = provider === "tailscale" ? null : logFile || (spawnFn ? null : createTunnelLogFile(provider));
|
|
1358
|
+
let child;
|
|
1359
|
+
try {
|
|
1360
|
+
const binary = provider === "tailscale" ? resolveTailscaleBinary((bin) => getExecutor().findExecutable(bin)) : provider;
|
|
1361
|
+
if (!binary) throw new Error("Install Tailscale and sign in to your tailnet.");
|
|
1362
|
+
const { bin, args } = tunnelArgv(provider, port, ngrokUrl, outputFile, binary);
|
|
1363
|
+
child = spawn(bin, args, {
|
|
1364
|
+
stdio: outputFile ? [
|
|
1365
|
+
"ignore",
|
|
1366
|
+
"ignore",
|
|
1367
|
+
"ignore"
|
|
1368
|
+
] : [
|
|
1369
|
+
"ignore",
|
|
1370
|
+
"pipe",
|
|
1371
|
+
"pipe"
|
|
1372
|
+
],
|
|
1373
|
+
detached: true
|
|
1374
|
+
});
|
|
1375
|
+
} catch (err) {
|
|
1376
|
+
removeTunnelLogFile(outputFile);
|
|
1377
|
+
return {
|
|
1378
|
+
failed: true,
|
|
1379
|
+
reason: `Could not start ${provider}: ${describe(err)}`
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
let childExited = false;
|
|
1383
|
+
child.once("exit", () => {
|
|
1384
|
+
childExited = true;
|
|
1385
|
+
});
|
|
1386
|
+
const cleanup = async (alreadyExited = childExited) => {
|
|
1387
|
+
const stopped = await terminateChild(child, {
|
|
1388
|
+
alreadyExited,
|
|
1389
|
+
timeoutMs: cleanupTimeoutMs,
|
|
1390
|
+
now,
|
|
1391
|
+
sleep,
|
|
1392
|
+
isAlive: isChildAlive
|
|
1393
|
+
});
|
|
1394
|
+
if (stopped) removeTunnelLogFile(outputFile);
|
|
1395
|
+
return stopped ? { status: "stopped" } : {
|
|
1396
|
+
status: "failed",
|
|
1397
|
+
reason: `Sent SIGKILL but could not confirm that pid ${child.pid ?? "unknown"} exited.`
|
|
1398
|
+
};
|
|
1399
|
+
};
|
|
1400
|
+
const failAfterCleanup = async (reason, alreadyExited = childExited) => {
|
|
1401
|
+
const stopped = await cleanup(alreadyExited);
|
|
1402
|
+
return stopped.status === "stopped" ? {
|
|
1403
|
+
failed: true,
|
|
1404
|
+
reason
|
|
1405
|
+
} : {
|
|
1406
|
+
failed: true,
|
|
1407
|
+
reason: `${reason} ${stopped.reason}`,
|
|
1408
|
+
cleanupFailed: true
|
|
1409
|
+
};
|
|
1410
|
+
};
|
|
1411
|
+
const pid = child.pid;
|
|
1412
|
+
if (!pid) return failAfterCleanup(`${provider} started but reported no pid.`);
|
|
1413
|
+
const captureChildIdentity = () => {
|
|
1414
|
+
try {
|
|
1415
|
+
return readProcessToken(pid);
|
|
1416
|
+
} catch {
|
|
1417
|
+
return null;
|
|
1418
|
+
}
|
|
1419
|
+
};
|
|
1420
|
+
const initialProcessToken = captureChildIdentity();
|
|
1421
|
+
if (!initialProcessToken) return failAfterCleanup(`${provider} started but its process identity token could not be read.`);
|
|
1422
|
+
const { url, exited, lastLine } = outputFile ? await waitForFileUrl(child, outputFile, parserFor(provider), urlTimeoutMs) : await waitForUrl(child, parserFor(provider), urlTimeoutMs);
|
|
1423
|
+
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);
|
|
1424
|
+
if (!outputFile) resumeChildPipes(child);
|
|
1425
|
+
if (requireReachable) {
|
|
1426
|
+
if (!await waitUntilReachable({
|
|
1427
|
+
url,
|
|
1428
|
+
now,
|
|
1429
|
+
sleep,
|
|
1430
|
+
probe: probeReachable,
|
|
1431
|
+
timeoutMs: reachableTimeoutMs
|
|
1432
|
+
})) return failAfterCleanup(`${url} did not become reachable within ${reachableTimeoutMs}ms; ${provider} may still be registering, or the network path is blocked.`);
|
|
1433
|
+
}
|
|
1434
|
+
const finalProcessToken = captureChildIdentity();
|
|
1435
|
+
if (!finalProcessToken || finalProcessToken !== initialProcessToken) return failAfterCleanup(`${provider} process identity changed before its tunnel could be recorded.`);
|
|
1436
|
+
if (!outputFile) unrefChildPipes(child);
|
|
1437
|
+
child.unref?.();
|
|
1438
|
+
if (childExited) return failAfterCleanup(`${provider} exited before its tunnel could be recorded.`, true);
|
|
1439
|
+
return {
|
|
1440
|
+
url,
|
|
1441
|
+
pid,
|
|
1442
|
+
processToken: initialProcessToken,
|
|
1443
|
+
logFile: outputFile,
|
|
1444
|
+
cleanup: () => cleanup()
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
async function startTunnelSequence({ providers, port, ngrokUrl = null, requireReachable = true, start = startTunnel }) {
|
|
1448
|
+
const failures = [];
|
|
1449
|
+
for (const provider of providers) {
|
|
1450
|
+
const result = await start({
|
|
1451
|
+
provider,
|
|
1452
|
+
port,
|
|
1453
|
+
ngrokUrl: provider === "ngrok" ? ngrokUrl : null,
|
|
1454
|
+
requireReachable
|
|
1455
|
+
});
|
|
1456
|
+
if (!("failed" in result)) return {
|
|
1457
|
+
provider,
|
|
1458
|
+
...result
|
|
1459
|
+
};
|
|
1460
|
+
failures.push(`${provider}: ${result.reason}`);
|
|
1461
|
+
if (result.cleanupFailed) return {
|
|
1462
|
+
failed: true,
|
|
1463
|
+
reason: failures.join(" ")
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
return {
|
|
1467
|
+
failed: true,
|
|
1468
|
+
reason: failures.join(" ") || "No managed tunnel provider was selected."
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
function closeChildPipes(child) {
|
|
1472
|
+
child.stdout?.destroy?.();
|
|
1473
|
+
child.stderr?.destroy?.();
|
|
1479
1474
|
}
|
|
1480
|
-
function
|
|
1481
|
-
|
|
1475
|
+
function unrefChildPipes(child) {
|
|
1476
|
+
resumeChildPipes(child);
|
|
1477
|
+
for (const stream of [child.stdout, child.stderr]) stream?.unref?.();
|
|
1482
1478
|
}
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1479
|
+
function resumeChildPipes(child) {
|
|
1480
|
+
child.stdout?.resume?.();
|
|
1481
|
+
child.stderr?.resume?.();
|
|
1482
|
+
}
|
|
1483
|
+
async function signalAndWaitForExit(child, signal, { timeoutMs, now, sleep, isAlive, platform }) {
|
|
1484
|
+
let exited = false;
|
|
1485
|
+
const onExit = () => {
|
|
1486
|
+
exited = true;
|
|
1487
|
+
};
|
|
1488
|
+
child.once("exit", onExit);
|
|
1489
|
+
try {
|
|
1490
|
+
if (platform === "win32" && child.pid) signalProcessTree(child.pid, signal, { platform });
|
|
1491
|
+
child.kill(signal);
|
|
1492
|
+
} catch (err) {
|
|
1493
|
+
child.removeListener("exit", onExit);
|
|
1494
|
+
return isEsrch(err);
|
|
1489
1495
|
}
|
|
1490
|
-
|
|
1496
|
+
const hasExited = () => exited;
|
|
1497
|
+
const deadline = now() + timeoutMs;
|
|
1498
|
+
while (!hasExited() && now() < deadline) await sleep(CLEANUP_POLL_MS);
|
|
1499
|
+
if (!hasExited() && child.pid && !isAlive(child.pid)) exited = true;
|
|
1500
|
+
child.removeListener("exit", onExit);
|
|
1501
|
+
return hasExited();
|
|
1491
1502
|
}
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1503
|
+
async function terminateChild(child, { alreadyExited, timeoutMs, now, sleep, isAlive, platform = process.platform }) {
|
|
1504
|
+
if (alreadyExited) {
|
|
1505
|
+
closeChildPipes(child);
|
|
1506
|
+
return true;
|
|
1507
|
+
}
|
|
1508
|
+
if (await signalAndWaitForExit(child, "SIGTERM", {
|
|
1509
|
+
timeoutMs,
|
|
1510
|
+
now,
|
|
1511
|
+
sleep,
|
|
1512
|
+
isAlive,
|
|
1513
|
+
platform
|
|
1514
|
+
})) {
|
|
1515
|
+
closeChildPipes(child);
|
|
1516
|
+
return true;
|
|
1517
|
+
}
|
|
1518
|
+
const killed = await signalAndWaitForExit(child, "SIGKILL", {
|
|
1519
|
+
timeoutMs,
|
|
1520
|
+
now,
|
|
1521
|
+
sleep,
|
|
1522
|
+
isAlive,
|
|
1523
|
+
platform
|
|
1524
|
+
});
|
|
1525
|
+
closeChildPipes(child);
|
|
1526
|
+
return killed;
|
|
1495
1527
|
}
|
|
1496
|
-
function
|
|
1497
|
-
|
|
1498
|
-
return bundleCandidates(getExecutor().runFile("ps", [
|
|
1499
|
-
"-axww",
|
|
1500
|
-
"-o",
|
|
1501
|
-
"pid=,comm="
|
|
1502
|
-
], { timeoutMs: 1e4 }), dirs).filter((pid) => pid !== process.pid);
|
|
1528
|
+
function describe(err) {
|
|
1529
|
+
return err?.message || String(err);
|
|
1503
1530
|
}
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1531
|
+
const STOP_TIMEOUT_MS = 5e3;
|
|
1532
|
+
const STOP_POLL_MS = 100;
|
|
1533
|
+
function isEsrch(err) {
|
|
1534
|
+
return err?.code === "ESRCH";
|
|
1507
1535
|
}
|
|
1508
|
-
function
|
|
1509
|
-
|
|
1536
|
+
async function stopTunnel(record, { isAlive = pidExists, inspectIdentity = process_identity_exports.inspectProcessIdentity, platform = process.platform, kill = (pid) => signalProcessTreeAsync(pid, "SIGTERM", { platform }), now = Date.now, sleep = defaultSleep, timeoutMs = STOP_TIMEOUT_MS } = {}) {
|
|
1537
|
+
const missing = () => {
|
|
1538
|
+
removeRecordedTunnelLogFile(record?.logFile);
|
|
1539
|
+
return { status: "missing" };
|
|
1540
|
+
};
|
|
1541
|
+
const pid = record?.pid;
|
|
1542
|
+
if (!pid || !isAlive(pid)) return missing();
|
|
1543
|
+
if (!record.processToken) return {
|
|
1544
|
+
status: "failed",
|
|
1545
|
+
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.`
|
|
1546
|
+
};
|
|
1547
|
+
const identity = inspectIdentity(record);
|
|
1548
|
+
if (identity === "gone" || identity === "different") return missing();
|
|
1549
|
+
if (identity !== "same") return {
|
|
1550
|
+
status: "failed",
|
|
1551
|
+
reason: `could not verify the process identity for tunnel pid ${pid}; refusing to signal it.`
|
|
1552
|
+
};
|
|
1553
|
+
try {
|
|
1554
|
+
await kill(pid);
|
|
1555
|
+
} catch (err) {
|
|
1556
|
+
return isEsrch(err) ? missing() : {
|
|
1557
|
+
status: "failed",
|
|
1558
|
+
reason: describe(err)
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
const deadline = now() + timeoutMs;
|
|
1562
|
+
const exited = () => {
|
|
1563
|
+
if (!isAlive(pid)) return true;
|
|
1564
|
+
const current = inspectIdentity(record);
|
|
1565
|
+
return current === "gone" || current === "different";
|
|
1566
|
+
};
|
|
1567
|
+
while (now() < deadline && !exited()) await sleep(STOP_POLL_MS);
|
|
1568
|
+
if (!exited()) return {
|
|
1569
|
+
status: "failed",
|
|
1570
|
+
reason: `pid ${pid} did not exit within ${timeoutMs}ms.`
|
|
1571
|
+
};
|
|
1572
|
+
removeRecordedTunnelLogFile(record.logFile);
|
|
1573
|
+
return { status: "stopped" };
|
|
1510
1574
|
}
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
const
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
const
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1575
|
+
//#endregion
|
|
1576
|
+
//#region src/supervisor/ownership.ts
|
|
1577
|
+
function resolveSupervisorTarget({ state, record, reservedPort, isAlive = pidExists, inspectIdentity = process_identity_exports.inspectProcessIdentity } = {}) {
|
|
1578
|
+
const statePid = numberOrNull(state?.pid);
|
|
1579
|
+
const recordPid = numberOrNull(record?.pid);
|
|
1580
|
+
const pid = statePid ?? recordPid;
|
|
1581
|
+
if (!pid) return { status: "none" };
|
|
1582
|
+
if (statePid && recordPid && statePid !== recordPid) {
|
|
1583
|
+
const gone = (entry) => ["gone", "different"].includes(inspectIdentity(entry));
|
|
1584
|
+
if (gone(state) && gone(record)) return {
|
|
1585
|
+
status: "stale",
|
|
1586
|
+
pid
|
|
1587
|
+
};
|
|
1588
|
+
return {
|
|
1589
|
+
status: "unverified",
|
|
1526
1590
|
pid,
|
|
1527
|
-
|
|
1528
|
-
startedAtMicros: start.startedAtMicros
|
|
1591
|
+
reason: `workspace state.json records supervisor pid ${statePid} but the registry records pid ${recordPid}`
|
|
1529
1592
|
};
|
|
1530
|
-
let open;
|
|
1531
|
-
try {
|
|
1532
|
-
open = getExecutor().runFile("lsof", [
|
|
1533
|
-
"-a",
|
|
1534
|
-
"-p",
|
|
1535
|
-
String(pid),
|
|
1536
|
-
"-d",
|
|
1537
|
-
"txt",
|
|
1538
|
-
"-Fn"
|
|
1539
|
-
], { timeoutMs: 1e4 });
|
|
1540
|
-
} catch (error) {
|
|
1541
|
-
if (["gone", "different"].includes((0, process_identity_exports.inspectProcessIdentity)(record))) continue;
|
|
1542
|
-
open = String(error.stdout ?? "");
|
|
1543
|
-
if (!open.trim()) throw unverified(pid, `lsof failed (${error.message.split("\n")[0]})`);
|
|
1544
|
-
}
|
|
1545
|
-
if (lsofNamesBundleExecutable(open, dirs)) instances.push(record);
|
|
1546
1593
|
}
|
|
1547
|
-
return
|
|
1594
|
+
if (!isAlive(pid)) return {
|
|
1595
|
+
status: "stale",
|
|
1596
|
+
pid
|
|
1597
|
+
};
|
|
1598
|
+
const port = numberOrNull(state?.port) ?? numberOrNull(record?.port);
|
|
1599
|
+
if (reservedPort !== null && reservedPort !== void 0 && port !== null && port !== reservedPort) return {
|
|
1600
|
+
status: "unverified",
|
|
1601
|
+
pid,
|
|
1602
|
+
reason: `supervisor pid ${pid} records port ${port}, but this project reserved port ${reservedPort}`
|
|
1603
|
+
};
|
|
1604
|
+
if (reservedPort !== null && reservedPort !== void 0 && port === null) return {
|
|
1605
|
+
status: "unverified",
|
|
1606
|
+
pid,
|
|
1607
|
+
reason: `supervisor pid ${pid} has no recorded port, so it cannot be matched against reserved port ${reservedPort}`
|
|
1608
|
+
};
|
|
1609
|
+
const identityRecord = statePid ? state : record;
|
|
1610
|
+
if (statePid && recordPid && state?.processToken !== record?.processToken) return {
|
|
1611
|
+
status: "unverified",
|
|
1612
|
+
pid,
|
|
1613
|
+
reason: "workspace state and registry process identities disagree"
|
|
1614
|
+
};
|
|
1615
|
+
const identity = inspectIdentity(identityRecord);
|
|
1616
|
+
if (identity === "gone" || identity === "different") return {
|
|
1617
|
+
status: "stale",
|
|
1618
|
+
pid
|
|
1619
|
+
};
|
|
1620
|
+
if (identity !== "same") return {
|
|
1621
|
+
status: "unverified",
|
|
1622
|
+
pid,
|
|
1623
|
+
reason: "the recorded process identity could not be verified"
|
|
1624
|
+
};
|
|
1625
|
+
return {
|
|
1626
|
+
status: "ours",
|
|
1627
|
+
processToken: identityRecord.processToken,
|
|
1628
|
+
pid,
|
|
1629
|
+
port,
|
|
1630
|
+
mode: state?.mode ?? record?.mode ?? null,
|
|
1631
|
+
startedAt: state?.startedAt ?? record?.startedAt ?? null
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
function numberOrNull(v) {
|
|
1635
|
+
return typeof v === "number" && Number.isSafeInteger(v) && v > 0 ? v : null;
|
|
1548
1636
|
}
|
|
1549
1637
|
//#endregion
|
|
1550
1638
|
//#region src/workspace/in-use.ts
|
|
@@ -1666,7 +1754,7 @@ function readRecordedRoot(dir, name) {
|
|
|
1666
1754
|
projectRoot: null,
|
|
1667
1755
|
problem: "its workspace.json records no absolute project root"
|
|
1668
1756
|
};
|
|
1669
|
-
if (workspaceName(projectRoot) !== name) return {
|
|
1757
|
+
if (workspaceName$1(projectRoot) !== name) return {
|
|
1670
1758
|
projectRoot: null,
|
|
1671
1759
|
problem: `its workspace.json records ${projectRoot}, whose workspace directory has another name`
|
|
1672
1760
|
};
|
|
@@ -1760,10 +1848,21 @@ function collectOrphanedWorkspaces(registryKeys, mountedVolumes, { measure = tru
|
|
|
1760
1848
|
isMounted: (path) => isOnMountedVolume(path, mountedVolumes),
|
|
1761
1849
|
inUse: (root) => workspaceInUse(root)
|
|
1762
1850
|
});
|
|
1763
|
-
return
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1851
|
+
if (!measure) return classified;
|
|
1852
|
+
return (async () => {
|
|
1853
|
+
const orphaned = [];
|
|
1854
|
+
for (const entry of classified.orphaned) {
|
|
1855
|
+
const bytes = await measuredDirectorySizeAsync(entry.dir, { timeoutMs: SIZE_TIMEOUT_MS });
|
|
1856
|
+
orphaned.push({
|
|
1857
|
+
...entry,
|
|
1858
|
+
bytes
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
return {
|
|
1862
|
+
orphaned,
|
|
1863
|
+
skipped: classified.skipped
|
|
1864
|
+
};
|
|
1865
|
+
})();
|
|
1767
1866
|
}
|
|
1768
1867
|
const REBUILD_COST = "The next build of an unchanged app installs from the shared build cache; after a native change the compilation cache speeds the rebuild, but on React Native 0.86 Swift does not use it (explicit modules are off), so that build recompiles Swift. The next stim macos in a cleared workspace is a full Swift build.";
|
|
1769
1868
|
const WORKSPACE_OUTPUT_DIRS = [
|
|
@@ -1789,9 +1888,6 @@ function planWorkspaceOutputs(entries, { olderThan, now }) {
|
|
|
1789
1888
|
});
|
|
1790
1889
|
}
|
|
1791
1890
|
const SIZE_TIMEOUT_MS = 6e4;
|
|
1792
|
-
function sizeOf(dir, timeoutMs = SIZE_TIMEOUT_MS) {
|
|
1793
|
-
return measuredDirectorySize(dir, { timeoutMs });
|
|
1794
|
-
}
|
|
1795
1891
|
function outputPaths(dir) {
|
|
1796
1892
|
const paths = WORKSPACE_OUTPUT_DIRS.map((name) => join$1(dir, name)).filter((path) => existsSync$1(path));
|
|
1797
1893
|
const macos = join$1(dir, "macos");
|
|
@@ -1801,10 +1897,10 @@ function outputPaths(dir) {
|
|
|
1801
1897
|
} catch {}
|
|
1802
1898
|
return paths;
|
|
1803
1899
|
}
|
|
1804
|
-
function outputBytes(paths, timeoutMs) {
|
|
1900
|
+
async function outputBytes(paths, timeoutMs) {
|
|
1805
1901
|
let total = 0;
|
|
1806
1902
|
for (const path of paths) {
|
|
1807
|
-
const size =
|
|
1903
|
+
const size = await measuredDirectorySizeAsync(path, { timeoutMs });
|
|
1808
1904
|
if (size === null) return null;
|
|
1809
1905
|
total += size;
|
|
1810
1906
|
}
|
|
@@ -1812,19 +1908,32 @@ function outputBytes(paths, timeoutMs) {
|
|
|
1812
1908
|
}
|
|
1813
1909
|
function collectWorkspaceOutputs({ olderThan, now, exclude = [], measure = true, sizeTimeoutMs = SIZE_TIMEOUT_MS }) {
|
|
1814
1910
|
const mountedVolumes = listMountedVolumes();
|
|
1815
|
-
const entries = listWorkspaceDirs().filter((entry) => !exclude.includes(entry.dir)).filter((entry) => entry.projectRoot === null || isOnMountedVolume(entry.projectRoot, mountedVolumes)).map((entry) => Object.assign({}, entry, { paths: outputPaths(entry.dir) })).filter((entry) => entry.paths.length > 0)
|
|
1816
|
-
|
|
1817
|
-
inUse: entry.projectRoot === null ? [] : workspaceInUse(entry.projectRoot)
|
|
1818
|
-
}));
|
|
1819
|
-
return {
|
|
1911
|
+
const entries = listWorkspaceDirs().filter((entry) => !exclude.includes(entry.dir)).filter((entry) => entry.projectRoot === null || isOnMountedVolume(entry.projectRoot, mountedVolumes)).map((entry) => Object.assign({}, entry, { paths: outputPaths(entry.dir) })).filter((entry) => entry.paths.length > 0);
|
|
1912
|
+
const report = (sizes) => ({
|
|
1820
1913
|
root: workspacesRoot(),
|
|
1821
|
-
workspaces: planWorkspaceOutputs(
|
|
1914
|
+
workspaces: planWorkspaceOutputs(sizes.map((entry) => ({
|
|
1915
|
+
...entry,
|
|
1916
|
+
lastUsed: entry.projectRoot === null ? NaN : workspaceLastUsed(entry.projectRoot),
|
|
1917
|
+
inUse: entry.projectRoot === null ? [] : workspaceInUse(entry.projectRoot)
|
|
1918
|
+
})), {
|
|
1822
1919
|
olderThan,
|
|
1823
1920
|
now
|
|
1824
1921
|
})
|
|
1825
|
-
};
|
|
1922
|
+
});
|
|
1923
|
+
if (!measure) return report(entries.map(({ paths: _paths, ...entry }) => ({
|
|
1924
|
+
...entry,
|
|
1925
|
+
bytes: null
|
|
1926
|
+
})));
|
|
1927
|
+
return (async () => {
|
|
1928
|
+
const sizes = [];
|
|
1929
|
+
for (const { paths, ...entry } of entries) sizes.push({
|
|
1930
|
+
...entry,
|
|
1931
|
+
bytes: await outputBytes(paths, sizeTimeoutMs)
|
|
1932
|
+
});
|
|
1933
|
+
return report(sizes);
|
|
1934
|
+
})();
|
|
1826
1935
|
}
|
|
1827
|
-
async function clearWorkspaceOutputs(report, { olderThan, now = Date.now() }) {
|
|
1936
|
+
async function clearWorkspaceOutputs(report, { olderThan, now = Date.now() }, { log, record } = GC_OUTPUT) {
|
|
1828
1937
|
let failures = 0;
|
|
1829
1938
|
let cleared = 0;
|
|
1830
1939
|
const clearedRoots = [];
|
|
@@ -1832,8 +1941,8 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now() }) {
|
|
|
1832
1941
|
const root = entry.projectRoot;
|
|
1833
1942
|
if (!entry.willClear || root === null) {
|
|
1834
1943
|
if (entry.keptReason) {
|
|
1835
|
-
|
|
1836
|
-
|
|
1944
|
+
log(chalk.dim(`Kept the build outputs of ${root ?? entry.dir}: ${entry.keptReason}`));
|
|
1945
|
+
record("workspaceOutputs", "kept", root ?? entry.dir, {
|
|
1837
1946
|
bytes: entry.bytes,
|
|
1838
1947
|
detail: entry.keptReason
|
|
1839
1948
|
});
|
|
@@ -1861,8 +1970,8 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now() }) {
|
|
|
1861
1970
|
}, { purpose: "gc" });
|
|
1862
1971
|
} catch (error) {
|
|
1863
1972
|
failures++;
|
|
1864
|
-
|
|
1865
|
-
|
|
1973
|
+
log(chalk.red(`Could not clear the build outputs of ${root}: ${error?.message || String(error)}`));
|
|
1974
|
+
record("workspaceOutputs", "failed", root, {
|
|
1866
1975
|
bytes: entry.bytes,
|
|
1867
1976
|
detail: error?.message || String(error)
|
|
1868
1977
|
});
|
|
@@ -1870,8 +1979,8 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now() }) {
|
|
|
1870
1979
|
}
|
|
1871
1980
|
const kept = run.ran ? run.value : `in use: ${run.reasons.join("; ")}`;
|
|
1872
1981
|
if (kept) {
|
|
1873
|
-
|
|
1874
|
-
|
|
1982
|
+
log(chalk.yellow(`Kept the build outputs of ${root}: ${kept}`));
|
|
1983
|
+
record("workspaceOutputs", "kept", root, {
|
|
1875
1984
|
bytes: entry.bytes,
|
|
1876
1985
|
detail: kept
|
|
1877
1986
|
});
|
|
@@ -1879,10 +1988,10 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now() }) {
|
|
|
1879
1988
|
}
|
|
1880
1989
|
cleared += entry.bytes ?? 0;
|
|
1881
1990
|
clearedRoots.push(root);
|
|
1882
|
-
|
|
1883
|
-
|
|
1991
|
+
log(chalk.green(`Cleared the build outputs of ${root} (${sizeText(entry.bytes)})`));
|
|
1992
|
+
record("workspaceOutputs", "done", root, { bytes: entry.bytes });
|
|
1884
1993
|
}
|
|
1885
|
-
if (cleared)
|
|
1994
|
+
if (cleared) log(chalk.dim(`Cleared ${formatBytes(cleared)} of workspace build outputs. ${REBUILD_COST}`));
|
|
1886
1995
|
return {
|
|
1887
1996
|
failures,
|
|
1888
1997
|
cleared: clearedRoots
|
|
@@ -1940,4 +2049,4 @@ function sizeText(bytes) {
|
|
|
1940
2049
|
return bytes === null ? "size unknown" : formatBytes(bytes);
|
|
1941
2050
|
}
|
|
1942
2051
|
//#endregion
|
|
1943
|
-
export {
|
|
2052
|
+
export { teardownParkedIosSim as A, takeGcResults as B, eraseParkedAvd as C, teardownOwnedBrowser as D, teardownOwnedAvd as E, acquireBuildSlot as F, listBuildSlots as I, releaseBuildSlot as L, canonical$1 as M, measureDu as N, teardownOwnedIosSim as O, writeUsageCache as P, GC_OUTPUT as R, runningBundleInstancesAsync as S, teardownBrowserHeld as T, terminateChild as _, collectWorkspaceOutputs as a, withManagedTunnelLock as b, listWorkspaceDirs as c, emptyWorkspaceDir as d, withIdleWorkspace as f, stopTunnel as g, startTunnelSequence as h, collectOrphanedWorkspaces as i, probePublicHttp as j, teardownParkedAvd as k, rootPresence as l, resolveSupervisorTarget 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, withManagedRemoteWorktreeLock as v, eraseParkedIosSim as w, withManagedTunnelRemovalLock as x, withManagedRemoteWorktreeRemovalLock as y, recordGcResult as z };
|