stim 1.20.2 → 1.22.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 +141 -8
- package/dist/acquire-artifact-Co2k4f7O.mjs +171 -0
- package/dist/act-DsuqEkiM.mjs +2801 -0
- package/dist/activity-CMLmzp7o.mjs +1397 -0
- package/dist/{agent-device-usage-output-Dzgv6lgR.mjs → agent-device-usage-output-DUFaTMdd.mjs} +23 -67
- package/dist/{android-DCKZrNvV.mjs → android-ByRMUoiJ.mjs} +157 -93
- package/dist/{android-Mz_Inn3w.mjs → android-C7sVmyI9.mjs} +299 -116
- package/dist/{android-Dlzxshgp.mjs → android-DGkNqa_c.mjs} +5 -5
- package/dist/{android-HqP1_vB_.mjs → android-DR5lZ2Az.mjs} +2 -2
- package/dist/android-cas-compiler.mjs +2 -2
- package/dist/{android-cas-Bl-cLJBk.mjs → android-cas-ngferVq2.mjs} +7 -6
- package/dist/android-project-BCAAmMwz.mjs +12 -0
- package/dist/android-project-DVWyCbWU.d.mts +391 -0
- package/dist/api.d.mts +810 -64
- package/dist/api.mjs +782 -1
- package/dist/{artifact-HSwkIWEQ.mjs → artifact-8EY8SAPw.mjs} +240 -213
- package/dist/{artifact-D3czlUvT.mjs → artifact-DJPy518Y.mjs} +203 -164
- package/dist/artifact-platform-CXcmX_j3.mjs +27 -0
- package/dist/{browser-web-CA-F1GQo.mjs → browser-web-Bcv99PBM.mjs} +194 -64
- package/dist/budget-Cvwn54qV.mjs +9285 -0
- package/dist/{build-DYDbfkk5.mjs → build-CabC3G23.mjs} +153 -98
- package/dist/build-cache-Dv7RN200.mjs +602 -0
- package/dist/build-plan-DUC8Ka9y.mjs +248 -0
- package/dist/{cache-manifest-XH-fvDUj.mjs → cache-manifest-DSeQjt-Q.mjs} +2 -2
- package/dist/cache-manifest.mjs +1 -1
- package/dist/{ccache-zdKiYQ3E.mjs → ccache-Btwc36-I.mjs} +10 -9
- package/dist/{cdp-Dq_ypAlJ.mjs → cdp-3fwICjzi.mjs} +18 -5
- package/dist/{chrome-BnzZMqsR.mjs → chrome-Bbw689-6.mjs} +7 -4
- package/dist/{cli-CLP3diBm.mjs → cli-CHZa9Lvk.mjs} +20 -20
- package/dist/cli.mjs +1 -1
- package/dist/client-OyRoEuSr.mjs +1577 -0
- package/dist/collector-run.d.mts +4 -40
- package/dist/collector-run.mjs +9 -8
- package/dist/{command-output-BTy8hZ0F.mjs → command-output-gwpm28fu.mjs} +1 -1
- package/dist/config-DlrsduwG.mjs +955 -0
- package/dist/{created-devices-Dzd5C2LF.mjs → created-devices-x9rN6bhs.mjs} +14 -4
- package/dist/{dependency-state-DG0p31-y.mjs → dependency-state-1I9KZT1Y.mjs} +20 -5
- package/dist/{detached-entry-B9C_zeQC.mjs → detached-entry-BwNEtIrP.mjs} +1 -1
- package/dist/{dev-client-DAWVIGBu.mjs → dev-client-BSDR5h3F.mjs} +2 -2
- package/dist/{device-BECOiufl.mjs → device-B-L3Fbxy.mjs} +91 -34
- package/dist/device-DklWZLqb.mjs +1068 -0
- package/dist/{device-capacity-CBUAcRyQ.mjs → device-capacity-DK5_EwRS.mjs} +42 -13
- package/dist/device-host-worker.mjs +76 -79
- package/dist/{device-ios-COSi6oS1.mjs → device-ios-DoB6KFCO.mjs} +77 -51
- package/dist/{device-lease-run-Cw2M4z51.mjs → device-lease-run-xJPQ_QQG.mjs} +14 -26
- package/dist/{device-pool-Bf-pOhSw.mjs → device-pool-BMkdsefq.mjs} +5 -5
- package/dist/{doctor-kfMBnUw6.mjs → doctor-Bo7BXLux.mjs} +271 -320
- package/dist/{doctor-CB8oZKCk.mjs → doctor-D5_EzwfA.mjs} +186 -91
- package/dist/doctor-fingerprint-DZLoYFVb.mjs +16 -0
- package/dist/doctor-fingerprint.mjs +19 -0
- package/dist/doctor-thjWAV0z.mjs +2 -0
- package/dist/{doctor-watchman-BRyV2adA.mjs → doctor-watchman-N2wDotwQ.mjs} +40 -21
- package/dist/{eas-build-JyfqyHfC.mjs → eas-build-CwknaQYj.mjs} +53 -20
- package/dist/{build-plan-B7SZsc2N.mjs → eas-fallback-CSgAulr5.mjs} +41 -214
- package/dist/{watchman-CPQFZLXY.mjs → eas-session-ledger-DlY_serQ.mjs} +230 -11
- package/dist/{error-diagnostics-BR677IDM.mjs → error-diagnostics-DBWO2rxE.mjs} +172 -82
- package/dist/exec-DEoAhcze.mjs +858 -0
- package/dist/exec-DnYc4HGH.d.mts +78 -0
- package/dist/{gc-qWOMdnER.mjs → gc-CXxN-4SE.mjs} +1091 -1029
- package/dist/gc-DnNDQyaP.mjs +3 -0
- package/dist/{guide-G9cAbVAc.mjs → guide-Br0LEfKy.mjs} +1165 -297
- package/dist/{hosted-android-C3MHGld1.mjs → hosted-android-B-9f-jkd.mjs} +10 -8
- package/dist/{hosted-client-ZEKpS9vw.mjs → hosted-client-DZAPrcro.mjs} +34 -19
- package/dist/{hosted-ios-l-o3iM9J.mjs → hosted-ios-CsCS21hn.mjs} +1 -1
- package/dist/{hosted-logs-EzQM7-3Z.mjs → hosted-logs-FIRx9X2f.mjs} +77 -43
- package/dist/{hosted-macos-BACVAVqE.mjs → hosted-macos-_awYSpfv.mjs} +35 -22
- package/dist/{hosted-native-A4s1QPXe.mjs → hosted-native-BdzE668-.mjs} +49 -27
- package/dist/idle-C4cZ757K.d.mts +24 -0
- package/dist/{idle-shutdown-DtxhPTKe.mjs → idle-shutdown-DDg3SdvY.mjs} +11 -10
- package/dist/ios-CDvlmUs7.d.mts +24 -0
- package/dist/{ios-BwVCF7oA.mjs → ios-Ds1clbIt.mjs} +8 -8
- package/dist/{ios-device-D7pi2Sca.mjs → ios-device--h_-8ENa.mjs} +51 -26
- package/dist/{ios-device-B5veSDDQ.mjs → ios-device-BXZ0c7Kb.mjs} +1 -1
- package/dist/{ios-RkW4pnQf.mjs → ios-hcBpnEGk.mjs} +200 -93
- package/dist/{ios-Ce7bc7br.mjs → ios-pm4nL6By.mjs} +160 -108
- package/dist/{ios-project-K9hY_Jab.mjs → ios-project-CZhKODF3.mjs} +13 -13
- package/dist/ios-signing-BtAffm0A.mjs +489 -0
- package/dist/{ios-state-CBLe1xYw.mjs → ios-state-Dtw5K2aI.mjs} +1 -1
- package/dist/{ios-nOLJGVdy.mjs → ios-xtlRHaeB.mjs} +1 -1
- package/dist/{js-swap-Bb3nsS07.mjs → js-swap-Bwhh4S7V.mjs} +30 -25
- package/dist/{launch-C3qJ6Agb.mjs → launch-BjxemNpK.mjs} +85 -543
- package/dist/{launch-F4sOqAhB.mjs → launch-CCzXlc8r.mjs} +91 -90
- package/dist/logs-BfwwadsZ.mjs +479 -0
- package/dist/logs-jTS_lHSo.mjs +248 -0
- package/dist/{macos-D7uDvO-z.mjs → macos-DVlSSyUg.mjs} +1 -1
- package/dist/macos-NfV2D7dO.mjs +694 -0
- package/dist/macos-run.mjs +1 -1
- package/dist/maintenance-run.mjs +123 -248
- package/dist/{metro-DnCEjCxs.mjs → metro-DiXtA3Ty.mjs} +107 -30
- package/dist/metro-gateway-CKZqKqx7.mjs +209 -0
- package/dist/{named-ports-4WopRPg-.mjs → named-ports-DkEzOfzO.mjs} +42 -90
- package/dist/native-android-CqgnOgsT.mjs +645 -0
- package/dist/native-gradle-artifact-inputs-DHFnmrVf.mjs +270 -0
- package/dist/native-gradle-inputs-Dnh84Zbd.mjs +211 -0
- package/dist/native-inputs-TRTTTRxt.mjs +91 -0
- package/dist/native-run-NwUuxbkg.mjs +276 -0
- package/dist/{native-runtime-k8dTIrbK.mjs → native-runtime-B7KIafpM.mjs} +11 -9
- package/dist/native-source-Y0yLhFyQ.mjs +133 -0
- package/dist/{native-xcode-inputs-CykopJdM.mjs → native-xcode-inputs-D_VOfFL7.mjs} +26 -18
- package/dist/{native-xcode-ios-Bbf30Gm0.mjs → native-xcode-ios-BYDOHWLN.mjs} +104 -60
- package/dist/{ndjson-GOnbyvso.mjs → ndjson-CgE_o3Dl.mjs} +3 -3
- package/dist/offload-worker.d.mts +59 -41
- package/dist/offload-worker.mjs +478 -115
- package/dist/operation-exec-BW0em61G.mjs +171 -0
- package/dist/{ownership-DCtw_2IA.mjs → ownership-C5fo5Edb.mjs} +3 -3
- package/dist/{page-DRHy329N.mjs → page-DBL2PiUU.mjs} +12 -5
- package/dist/paths-BsJMoPP2.mjs +12 -0
- package/dist/paths-DQtTgsBf.mjs +42 -0
- package/dist/{placement-log-MOfPVgaM.mjs → placement-log-BnGgJRRZ.mjs} +5 -5
- package/dist/{plan-placement-V0TdzYXp.mjs → plan-placement-DIsMtN-d.mjs} +15 -11
- package/dist/{ports-DdSUicVo.mjs → ports-Cy1RkmuQ.mjs} +3 -3
- package/dist/prebuild-D4JoEAEs.mjs +484 -0
- package/dist/{project-_phNIJfs.mjs → project-wM_XFTvE.mjs} +65 -37
- package/dist/{pull-request-CQ8sPbZa.mjs → pull-request-W1IuLXDt.mjs} +102 -20
- package/dist/pull-request-eODyIqZF.d.mts +124 -0
- package/dist/pull-requests.d.mts +2 -120
- package/dist/pull-requests.mjs +2 -2
- package/dist/{react-native-android-CM1XRDwv.mjs → react-native-android-6OZ7MSM7.mjs} +121 -72
- package/dist/{gradle-DSrXb6lG.mjs → react-native-build-Dvd8hC21.mjs} +292 -15
- package/dist/react-native-doctor-DsoSF1Y9.mjs +234 -0
- package/dist/{react-native-ios-Dhq_eiYQ.mjs → react-native-ios-4u1Rn-GR.mjs} +71 -39
- package/dist/{recordings-D0d5aQt0.mjs → recordings-CQmfzAh7.mjs} +12 -9
- package/dist/registration-BcRgF6Ox.mjs +51 -0
- package/dist/{reload-C8JhxjSh.mjs → reload-BzkYabB-.mjs} +94 -47
- package/dist/reload-D7AVlb8N.mjs +2 -0
- package/dist/{remote-BiR6aYjy.mjs → remote-DjGDmlTZ.mjs} +26 -18
- package/dist/result-D2Ykz_iD.mjs +435 -0
- package/dist/{run-BtPgpKks.mjs → run-DidHghD1.mjs} +5 -5
- package/dist/selection-DSMbWEX2.mjs +620 -0
- package/dist/{server-bare-R2m_gsH8.mjs → server-bare-CafzuyFB.mjs} +3 -4
- package/dist/{server-command-CTtCkmcQ.mjs → server-command-C6QjeupS.mjs} +14 -7
- package/dist/{server-expo-DsguMGUF.mjs → server-expo-CrjB29pq.mjs} +54 -11
- package/dist/server-expo-D58c9L0M.mjs +2 -0
- package/dist/{settings-B717odTD.mjs → settings-C7O8A6ZI.mjs} +309 -119
- package/dist/settings-DpR-1Wnn.mjs +114 -0
- package/dist/settings-write-CzD60rOD.mjs +360 -0
- package/dist/settings.schema.json +176 -16
- package/dist/{simslim-CCr3rE-A.mjs → simslim-BNckWpXi.mjs} +14 -12
- package/dist/{slot-launch-DPt0m9Nd.mjs → slot-launch-CbfmNgM6.mjs} +15 -20
- package/dist/{spawn-entry-Dcb7sDUz.mjs → spawn-entry-jw19m8Nl.mjs} +0 -1
- package/dist/stage-Bps5oius.mjs +200 -0
- package/dist/{start-TXeMmmO2.mjs → start-Br-oxqMD.mjs} +1 -1
- package/dist/start-EojN4WFz.mjs +1364 -0
- package/dist/{state-D1Icyjlu.mjs → state--smjFSTL.mjs} +55 -13
- package/dist/{state-CG_OFoaA.mjs → state-08I3hw3_.mjs} +3 -2
- package/dist/{stats-BoY5sEPU.mjs → stats-D3GerlFW.mjs} +7 -13
- package/dist/stats-report-BzaPrRm6.mjs +16 -0
- package/dist/{status-BTA7kKiQ.mjs → status-BRROykC8.mjs} +7 -5
- package/dist/status-EvrK72L2.mjs +2 -0
- package/dist/{status-WgFbkRzj.mjs → status-WBDajew2.mjs} +554 -452
- package/dist/{status-watch-C23ILtf9.mjs → status-watch-Bg_u-pwR.mjs} +222 -25
- package/dist/status-worker.d.mts +1 -0
- package/dist/status-worker.mjs +19 -0
- package/dist/stim-desktop-D5OGpYZM.mjs +112 -0
- package/dist/{stop-DR6VoDIk.mjs → stop-DTZ9uTw3.mjs} +2 -2
- package/dist/{stop-DPEhunlw.mjs → stop-Dky10AEJ.mjs} +33 -23
- package/dist/{stop-BNmBMi1F.mjs → stop-Mv6W9FAk.mjs} +137 -98
- package/dist/supervisor-run.d.mts +8 -17
- package/dist/supervisor-run.mjs +56 -28
- package/dist/{support-CwjbFs9H.mjs → support-CNioewsd.mjs} +14 -14
- package/dist/{support-DMTqj3Hm.mjs → support-yUiOjnVY.mjs} +20 -13
- package/dist/swiftpm-artifact-inputs-DFcGsXDP.mjs +153 -0
- package/dist/swiftpm-macos-BIak-Rxv.mjs +532 -0
- package/dist/toolchain-BY8X8HXx.mjs +2 -0
- package/dist/toolchain-DpJMHq0n.mjs +876 -0
- package/dist/{trigger-CigAYjeL.mjs → trigger-Cbyc41d_.mjs} +21 -6
- package/dist/trigger-yb_nFzPF.mjs +2 -0
- package/dist/{warm-progress-BuY-Fvfn.mjs → warm-progress-CA3TgT3-.mjs} +263 -156
- package/dist/web-CZ07gd77.mjs +2 -0
- package/dist/{web-BDBMxDpx.mjs → web-DKx6yoFA.mjs} +20 -14
- package/dist/web-run.mjs +13 -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-state-DJt_MMvf.mjs → workspace-state-U08dEzhG.mjs} +62 -58
- package/dist/{workspaces-C36s2jb9.mjs → workspaces-CpANmBlq.mjs} +1723 -1286
- package/dist/{worktree-CgvJYJ-Q.mjs → worktree-CHecZ4Oq.mjs} +1097 -547
- package/dist/worktree-ZqZVjD9K.mjs +2 -0
- package/dist/xcode-BXn9-xYx.mjs +896 -0
- package/package.json +7 -5
- package/shim/android-audio/Main.java +24 -0
- package/shim/android-audio/README.md +17 -0
- package/shim/android-audio.jar +0 -0
- 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/macos-Ch5VJmcS.mjs +0 -584
- package/dist/metro-gateway-CX9Xlkq2.mjs +0 -103
- package/dist/miss-reason-mKzZ4VvN.mjs +0 -238
- package/dist/native-android-D5qc3-_v.mjs +0 -226
- package/dist/native-gradle-inputs-BsLRrl7u.mjs +0 -349
- 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/settings-vk3mJGSm.d.mts +0 -13
- package/dist/spawn-claims-DVS5RkMS.mjs +0 -72
- package/dist/stage-KNTQopqv.mjs +0 -157
- package/dist/start-BWqzODpb.mjs +0 -1116
- package/dist/stim-desktop-BzBN2Em2.mjs +0 -63
- package/dist/swiftpm-macos-DRNVJWlb.mjs +0 -119
- 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,40 +1,47 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { d as
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { $ as
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
1
|
+
import { c as declareSpawnsOn, f as stopDeclaringSpawnsOn, g as process_identity_exports, h as withDrainingCancellation, r as WorkspaceProcessCleanupError, t as getExecutor, x as ownership_claim_exports } from "./exec-DEoAhcze.mjs";
|
|
2
|
+
import { d as workspaceDir } from "./paths-DQtTgsBf.mjs";
|
|
3
|
+
import { F as isOnMountedVolume, G as projectDeviceSlots, I as listMountedVolumes, L as measuredDirectorySizeAsync, N as directorySizeAsync, O as withConfigLockAsync$1, P as formatBytes, R as volumeRootFor, S as removeProject, _ as loadConfig$1, d as getConfigDir, j as acquireAvdClaim } from "./config-DlrsduwG.mjs";
|
|
4
|
+
import { h as shortUdid, o as formatLongDuration, u as plural } from "./command-output-gwpm28fu.mjs";
|
|
5
|
+
import { f as throwIfCancelled, t as runOperationFile } from "./operation-exec-BW0em61G.mjs";
|
|
6
|
+
import { u as workspaceLastUsed } from "./workspace-state-U08dEzhG.mjs";
|
|
7
|
+
import { Et as listWorktreesAsync, It as resolveSourceCheckoutAsync } from "./settings-C7O8A6ZI.mjs";
|
|
8
|
+
import { i as findProjectRoot$1, v as detectIsExpo } from "./project-wM_XFTvE.mjs";
|
|
9
|
+
import { n as easMachineStateRoot$1, o as removeEasSessionClaimAsync, r as readEasSessionLedger$1 } from "./eas-session-ledger-DlY_serQ.mjs";
|
|
10
|
+
import { $ as includesRecordings, At as unverifiedDeviceCommand, C as collectIdleDevices, D as idleDeviceLines, Dt as findOrphanedDevices, Et as eraseParkedDevices, Ft as withRemoteSessionGcLocks, In as runProcessOperation, Kt as withEasProjectLock, Mt as collectEasSessionSweep, Nt as deleteEasSessions, O as parseIdleDuration, Ot as findStaleDeviceRecords, Pt as describeError, Q as includesParkedDevices, Sr as sizeCaches, St as deleteProjectDevices, Tt as deviceSweepIsScoped, X as emptyCaches, Z as includesArchives, _r as phase, _t as collectDeviceLeases, at as selectsBeyondCaches, br as discoverCaches, bt as deleteParkedAvds, dr as memoryScope, dt as gradleVersionsPending, et as includesWorkspaceOutputs, fr as memorySweepIsScoped, ft as includesGradleVersions, gt as DEVICE_LIST_TIMEOUT_MS, hr as scopedMemoryReport, ht as collectInventory, ir as parseArchiveSelector, it as selectCaches, jt as withAndroidAvdSizes, k as shutDownIdleDevices, kt as findStaleProjectDevices, lr as memoryCacheKind, lt as deleteGradleVersions, mr as reclaimMemory, mt as getMachineCacheReport, nt as planCacheEmptying, ot as trimCaches, pr as memoryWorkPending, rt as previewScopedEmptying, sr as collectMemoryReport, st as collectGradleVersions, tt as isWorkspacesScope, ur as memoryLines, ut as gradleVersionLines, vt as collectParkedAvds, w as collectIdleDevicesAsync, wt as describeUnverifiableDevices, xt as deleteParkedSims, yt as collectParkedSims } from "./budget-Cvwn54qV.mjs";
|
|
11
|
+
import { t as canonicalPath } from "./paths-BsJMoPP2.mjs";
|
|
12
|
+
import { B as createGcResults, L as listBuildSlots, a as collectWorkspaceOutputs, c as listWorkspaceDirs, f as withIdleWorkspace, i as collectOrphanedWorkspaces, l as rootPresence, m as workspaceInUseAsync, n as WORKSPACE_OUTPUT_DIRS, o as deleteOrphanedWorkspaces, r as clearWorkspaceOutputs, s as isInsideWorkspaces, t as REBUILD_COST, u as sizeText, z as GC_OUTPUT } from "./workspaces-CpANmBlq.mjs";
|
|
13
|
+
import { N as listBuildLocks, k as removeExpiredLease } from "./activity-CMLmzp7o.mjs";
|
|
14
|
+
import { n as forgetCreatedDeviceAsync, r as readCreatedDevices$1 } from "./created-devices-x9rN6bhs.mjs";
|
|
15
|
+
import { F as listAvdsAsync, L as listOrphanedAvdDirectories, V as ownedAvdDirectory, _t as parkedMaxSetting, dt as POOL_SETTING_REMEDY, h as avdNameAbsent } from "./android-C7sVmyI9.mjs";
|
|
16
|
+
import { d as listAllIosSims, f as listAllIosSimsAsync } from "./ios-hcBpnEGk.mjs";
|
|
17
|
+
import { r as readCollectors } from "./ownership-C5fo5Edb.mjs";
|
|
18
|
+
import { a as matchWorktreeEntry, b as deleteSelectedArchives, v as describeDereferenced, x as sweepArchiveStaging, y as reclaimProject } from "./worktree-CHecZ4Oq.mjs";
|
|
19
|
+
import { s as writeCacheFile } from "./warm-progress-CA3TgT3-.mjs";
|
|
20
|
+
import { _ as collectWorktreeSweep, a as previewMaintenance, b as worktreeRemovalReason, c as deleteTempLeftovers, d as ARTIFACT_DIRS, f as appRoots, g as candidateRootsAsync, h as ownedDeviceUsage, l as tempLeftoverLines, o as triggeringRoot, p as finishedWorkspaces, s as collectTempLeftovers, t as deleteIdleWorkspaceDevices, u as tempLeftoverSection, v as removeWorktrees, y as worktreePullRequestNote } from "./act-DsuqEkiM.mjs";
|
|
21
|
+
import { i as getSwiftpmCacheUsage, n as swiftpmCacheLines, r as getAgentDeviceUsage, t as agentDeviceLines } from "./agent-device-usage-output-DUFaTMdd.mjs";
|
|
22
|
+
import { existsSync, lstatSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
23
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
24
|
+
import { LOG_ROTATE_BYTES, ensurePrivateDir, rotatedLogPath, withDirLock, withDirLockTransaction } from "@stim-cli/core";
|
|
25
|
+
import { assertHostedDeviceLedger, configLockPath, deviceHostArea, deviceHostRoot, diskUsageCacheFile, hostedDeviceId, isJsonObject, listRecordedDevicesAsync, listStatusCacheEntries, maintenanceRunClaims, parseHostedPlatformDevice, readArchivesAsync, readDiskUsage, readHostedDevice, readHostedDeviceLedger, readHostedSessions, recordedBytes } from "@stim-cli/core/state";
|
|
26
|
+
import { isAbsolute as isAbsolute$1, join as join$1 } from "path";
|
|
23
27
|
import { fileURLToPath } from "node:url";
|
|
24
|
-
import {
|
|
25
|
-
import { LOG_ROTATE_BYTES, rotatedLogPath } from "@stim-cli/core";
|
|
26
|
-
import { InvalidArgumentError } from "commander";
|
|
28
|
+
import { setImmediate } from "node:timers/promises";
|
|
27
29
|
import chalk from "chalk";
|
|
30
|
+
import { existsSync as existsSync$1 } from "fs";
|
|
31
|
+
import { InvalidArgumentError } from "commander";
|
|
32
|
+
import { open, readdir, rename, rm, stat, utimes, writeFile } from "node:fs/promises";
|
|
28
33
|
//#region src/commands/gc/archives.ts
|
|
29
|
-
function stagingOrNone() {
|
|
34
|
+
async function stagingOrNone(signal) {
|
|
30
35
|
try {
|
|
31
|
-
return sweepArchiveStaging(false, 5e3);
|
|
36
|
+
return await sweepArchiveStaging(false, 5e3, signal);
|
|
32
37
|
} catch {
|
|
38
|
+
signal?.throwIfAborted();
|
|
33
39
|
return [];
|
|
34
40
|
}
|
|
35
41
|
}
|
|
36
|
-
function collectArchives(scope, olderThan, now) {
|
|
37
|
-
|
|
42
|
+
async function collectArchives(scope, olderThan, now, signal) {
|
|
43
|
+
signal?.throwIfAborted();
|
|
44
|
+
const records = await readArchivesAsync(signal);
|
|
38
45
|
const { kind, id } = parseArchiveSelector(scope) ?? {
|
|
39
46
|
kind: null,
|
|
40
47
|
id: null
|
|
@@ -53,7 +60,7 @@ function collectArchives(scope, olderThan, now) {
|
|
|
53
60
|
expires: record.expires,
|
|
54
61
|
willDelete: (id === null && olderThan !== null ? now - Date.parse(record.removedAt) >= olderThan * 864e5 : true) && (!kind || record.bytes[kind] > 0)
|
|
55
62
|
})),
|
|
56
|
-
staging: stagingOrNone(),
|
|
63
|
+
staging: await stagingOrNone(signal),
|
|
57
64
|
unknownId: id !== null && !match ? id : null,
|
|
58
65
|
knownIds: records.map((record) => record.id),
|
|
59
66
|
kind
|
|
@@ -67,467 +74,50 @@ function archiveLines(selection) {
|
|
|
67
74
|
...selection.staging.map((entry) => ` ${entry.path}: ${entry.kept ? `kept: ${entry.reason}` : "abandoned staging"}${entry.removeCommand ? `; ${entry.removeCommand}` : ""}`)
|
|
68
75
|
];
|
|
69
76
|
}
|
|
70
|
-
function deleteArchives(selection) {
|
|
77
|
+
async function deleteArchives(selection, output = GC_OUTPUT, signal) {
|
|
78
|
+
signal?.throwIfAborted();
|
|
71
79
|
if (!selection) return 0;
|
|
72
80
|
let failures = 0;
|
|
73
|
-
for (const record of selection.records.filter((entry) => entry.willDelete))
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
for (const record of selection.records.filter((entry) => entry.willDelete)) {
|
|
82
|
+
signal?.throwIfAborted();
|
|
83
|
+
try {
|
|
84
|
+
await deleteSelectedArchives([record.id], selection.kind, signal);
|
|
85
|
+
output.record("archive", "done", record.id, {
|
|
86
|
+
id: record.id,
|
|
87
|
+
bytes: record.bytes
|
|
88
|
+
});
|
|
89
|
+
} catch (error) {
|
|
90
|
+
signal?.throwIfAborted();
|
|
91
|
+
failures++;
|
|
92
|
+
output.record("archive", "failed", record.id, {
|
|
93
|
+
id: record.id,
|
|
94
|
+
detail: error.message
|
|
95
|
+
});
|
|
96
|
+
output.error(`Could not delete ${record.id}: ${error.message}`);
|
|
97
|
+
}
|
|
86
98
|
}
|
|
87
99
|
try {
|
|
88
|
-
const remaining = new Set(sweepArchiveStaging(true, 5e3).map((entry) => entry.path));
|
|
89
|
-
for (const entry of selection.staging) if (!entry.kept && !remaining.has(entry.path))
|
|
100
|
+
const remaining = new Set((await sweepArchiveStaging(true, 5e3, signal)).map((entry) => entry.path));
|
|
101
|
+
for (const entry of selection.staging) if (!entry.kept && !remaining.has(entry.path)) output.record("archive", "done", "archive staging", { id: entry.path });
|
|
90
102
|
} catch (error) {
|
|
103
|
+
signal?.throwIfAborted();
|
|
91
104
|
failures++;
|
|
92
|
-
|
|
105
|
+
output.error(`Could not clear archive staging: ${error.message}`);
|
|
93
106
|
}
|
|
94
107
|
return failures;
|
|
95
108
|
}
|
|
96
109
|
function archiveRefusal(selection) {
|
|
97
110
|
if (selection?.unknownId === void 0 || selection.unknownId === null) return null;
|
|
98
|
-
process.exitCode = 1;
|
|
99
|
-
const message = `Unknown archive "${selection.unknownId}".`;
|
|
100
|
-
const remedy = `Known archive ids: ${selection.knownIds.join(", ") || "(none)"}`;
|
|
101
|
-
console.error(chalk.red(message));
|
|
102
|
-
console.error(chalk.dim(remedy));
|
|
103
|
-
console.error(chalk.red("failed: STIM_BAD_ARG"));
|
|
104
111
|
return {
|
|
105
112
|
code: "STIM_BAD_ARG",
|
|
106
|
-
message
|
|
107
|
-
remedy
|
|
113
|
+
message: `Unknown archive "${selection.unknownId}".`,
|
|
114
|
+
remedy: `Known archive ids: ${selection.knownIds.join(", ") || "(none)"}`
|
|
108
115
|
};
|
|
109
116
|
}
|
|
110
117
|
function archiveWorkPending(selection, otherWork) {
|
|
111
118
|
return otherWork || Boolean(selection?.records.some((record) => record.willDelete) || selection?.staging.some((entry) => !entry.kept));
|
|
112
119
|
}
|
|
113
120
|
//#endregion
|
|
114
|
-
//#region src/commands/gc/eas-sessions.ts
|
|
115
|
-
const EAS_OPERATION_TIMEOUT_MS = 3e4;
|
|
116
|
-
const EAS_COLLECTION_TIMEOUT_MS = 6e4;
|
|
117
|
-
const EAS_MAX_LIST_PAGES = 100;
|
|
118
|
-
const EAS_LOCK_WAIT_MS = 0;
|
|
119
|
-
const EAS_LOCK_SNAPSHOT_RETRIES = 3;
|
|
120
|
-
const PROXY_CREDENTIAL_ENV = ["AGENT_DEVICE_DAEMON_BASE_URL", "AGENT_DEVICE_DAEMON_AUTH_TOKEN"];
|
|
121
|
-
function describeError(error) {
|
|
122
|
-
const candidate = error;
|
|
123
|
-
if (typeof candidate?.stderr === "string" && candidate.stderr.trim()) return candidate.stderr.trim();
|
|
124
|
-
if (typeof candidate?.message === "string" && candidate.message.trim()) return candidate.message.trim();
|
|
125
|
-
return String(error);
|
|
126
|
-
}
|
|
127
|
-
function readRecordedRemoteSessionIds(roots) {
|
|
128
|
-
const ids = /* @__PURE__ */ new Set();
|
|
129
|
-
const notices = [];
|
|
130
|
-
let safe = true;
|
|
131
|
-
for (const root of new Set(roots)) {
|
|
132
|
-
try {
|
|
133
|
-
if (!statSync$1(root).isDirectory()) throw new Error("path is not a directory");
|
|
134
|
-
readdirSync$1(root);
|
|
135
|
-
} catch (error) {
|
|
136
|
-
safe = false;
|
|
137
|
-
notices.push(`${root} is not available as a readable workspace directory: ${describeError(error)}`);
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
const path = workspaceStateFile(root);
|
|
141
|
-
if (!existsSync$1(path)) continue;
|
|
142
|
-
let state;
|
|
143
|
-
try {
|
|
144
|
-
state = JSON.parse(readFileSync$1(path, "utf-8"));
|
|
145
|
-
} catch (error) {
|
|
146
|
-
safe = false;
|
|
147
|
-
notices.push(`${path} could not be read as valid JSON: ${describeError(error)}`);
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
if (!state || typeof state !== "object" || Array.isArray(state)) {
|
|
151
|
-
safe = false;
|
|
152
|
-
notices.push(`${path} does not contain a valid workspace state object.`);
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
const remote = state.remoteDevice;
|
|
156
|
-
if (remote === void 0) continue;
|
|
157
|
-
if (!remote || typeof remote !== "object" || Array.isArray(remote)) {
|
|
158
|
-
safe = false;
|
|
159
|
-
notices.push(`${path} has a malformed remote session record.`);
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
const id = remote.sessionId;
|
|
163
|
-
if (typeof id !== "string" || id.length === 0) {
|
|
164
|
-
safe = false;
|
|
165
|
-
notices.push(`${path} has a remote session record with no valid session id.`);
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
ids.add(id);
|
|
169
|
-
}
|
|
170
|
-
return {
|
|
171
|
-
ids: [...ids],
|
|
172
|
-
notices,
|
|
173
|
-
safe
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
function claimStateAbsence(claim) {
|
|
177
|
-
const stateDir = dirname$1(claim.stateFile);
|
|
178
|
-
let entries;
|
|
179
|
-
try {
|
|
180
|
-
if (!statSync$1(stateDir).isDirectory()) throw new Error("path is not a directory");
|
|
181
|
-
entries = readdirSync$1(stateDir);
|
|
182
|
-
} catch (error) {
|
|
183
|
-
return {
|
|
184
|
-
absent: false,
|
|
185
|
-
notice: `${stateDir} is not available for EAS ownership verification: ${describeError(error)}`
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
if (!entries.includes(basename$1(claim.stateFile))) return {
|
|
189
|
-
absent: true,
|
|
190
|
-
notice: null
|
|
191
|
-
};
|
|
192
|
-
let state;
|
|
193
|
-
try {
|
|
194
|
-
state = JSON.parse(readFileSync$1(claim.stateFile, "utf-8"));
|
|
195
|
-
} catch (error) {
|
|
196
|
-
return {
|
|
197
|
-
absent: false,
|
|
198
|
-
notice: `${claim.stateFile} could not be read as valid JSON: ${describeError(error)}`
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
if (!state || typeof state !== "object" || Array.isArray(state)) return {
|
|
202
|
-
absent: false,
|
|
203
|
-
notice: `${claim.stateFile} does not contain a valid workspace state object.`
|
|
204
|
-
};
|
|
205
|
-
const remote = state.remoteDevice;
|
|
206
|
-
if (remote === void 0) return {
|
|
207
|
-
absent: true,
|
|
208
|
-
notice: null
|
|
209
|
-
};
|
|
210
|
-
if (!remote || typeof remote !== "object" || Array.isArray(remote)) return {
|
|
211
|
-
absent: false,
|
|
212
|
-
notice: `${claim.stateFile} has a malformed remote session record.`
|
|
213
|
-
};
|
|
214
|
-
const sessionId = remote.sessionId;
|
|
215
|
-
if (typeof sessionId !== "string" || !sessionId) return {
|
|
216
|
-
absent: false,
|
|
217
|
-
notice: `${claim.stateFile} has a remote session record with no valid session id.`
|
|
218
|
-
};
|
|
219
|
-
return {
|
|
220
|
-
absent: sessionId !== claim.sessionId,
|
|
221
|
-
notice: null
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
function collectEasSessionSweep(config, deps) {
|
|
225
|
-
const projectRoot = (deps.findProjectRoot ?? findProjectRoot)(process.cwd());
|
|
226
|
-
const empty = (notices = []) => ({
|
|
227
|
-
projectScope: projectRoot,
|
|
228
|
-
orphaned: [],
|
|
229
|
-
notices,
|
|
230
|
-
deletionSafe: notices.length === 0
|
|
231
|
-
});
|
|
232
|
-
if (!projectRoot || !(deps.detectIsExpo ?? detectIsExpo)(projectRoot)) return empty();
|
|
233
|
-
if (deps.easSweepBlockedNotice) return empty([deps.easSweepBlockedNotice]);
|
|
234
|
-
const eas = (deps.resolveEasCliBin ?? resolveEasCliBin)(projectRoot);
|
|
235
|
-
if (!eas) return empty(["EAS session sweep skipped: eas-cli is not available for the current Expo project."]);
|
|
236
|
-
const recorded = readRecordedRemoteSessionIds([...Object.keys(config?.projects ?? {}), projectRoot]);
|
|
237
|
-
const ledgerRoot = deps.easLedgerRoot ?? easMachineStateRoot$1();
|
|
238
|
-
const ledger = readEasSessionLedger$1(ledgerRoot);
|
|
239
|
-
const run = deps.runEasFile ?? ((file, args, options) => getExecutor().runFile(file, args, options));
|
|
240
|
-
const sessions = [];
|
|
241
|
-
const seenCursors = /* @__PURE__ */ new Set();
|
|
242
|
-
const now = deps.easNow ?? Date.now;
|
|
243
|
-
const collectionTimeoutMs = deps.easCollectionTimeoutMs ?? EAS_COLLECTION_TIMEOUT_MS;
|
|
244
|
-
const maxPages = deps.easMaxPages ?? EAS_MAX_LIST_PAGES;
|
|
245
|
-
const deadline = now() + collectionTimeoutMs;
|
|
246
|
-
let pageCount = 0;
|
|
247
|
-
let after = null;
|
|
248
|
-
while (true) {
|
|
249
|
-
if (pageCount >= maxPages) return empty([...recorded.notices, `EAS session list exceeded the maximum page limit of ${maxPages} for ${projectRoot}.`]);
|
|
250
|
-
const remainingMs = deadline - now();
|
|
251
|
-
if (remainingMs <= 0) return empty([...recorded.notices, `EAS session list exceeded its total time limit for ${projectRoot}.`]);
|
|
252
|
-
let stdout;
|
|
253
|
-
try {
|
|
254
|
-
stdout = run(eas.file, listOwnedSessionsArgs(after), {
|
|
255
|
-
cwd: projectRoot,
|
|
256
|
-
timeoutMs: Math.min(EAS_OPERATION_TIMEOUT_MS, remainingMs),
|
|
257
|
-
omitEnv: PROXY_CREDENTIAL_ENV
|
|
258
|
-
});
|
|
259
|
-
} catch (error) {
|
|
260
|
-
return empty([...recorded.notices, `EAS session list failed for ${projectRoot}: ${describeError(error)}`]);
|
|
261
|
-
}
|
|
262
|
-
pageCount++;
|
|
263
|
-
if (now() > deadline) return empty([...recorded.notices, `EAS session list exceeded its total time limit for ${projectRoot}.`]);
|
|
264
|
-
const parsed = parseSessionListPage(stdout);
|
|
265
|
-
if (!parsed.ok) return empty([...recorded.notices, parsed.reason]);
|
|
266
|
-
sessions.push(...parsed.page.sessions);
|
|
267
|
-
if (!parsed.page.hasNextPage) break;
|
|
268
|
-
const cursor = parsed.page.endCursor?.trim();
|
|
269
|
-
if (!cursor) return empty([...recorded.notices, "EAS session list has a next page but returned no pagination cursor."]);
|
|
270
|
-
if (seenCursors.has(cursor)) return empty([...recorded.notices, `EAS session list repeated pagination cursor ${cursor}.`]);
|
|
271
|
-
seenCursors.add(cursor);
|
|
272
|
-
after = cursor;
|
|
273
|
-
}
|
|
274
|
-
const lockedRoots = deps.easLockedRoots;
|
|
275
|
-
if (lockedRoots) {
|
|
276
|
-
if (easLockRoots(projectRoot, loadConfig$1()).some((root) => !lockedRoots.includes(root))) return empty([...recorded.notices, "EAS session sweep skipped: registered workspace roots changed after remote-session locks were acquired."]);
|
|
277
|
-
}
|
|
278
|
-
const compared = findOrphanedOwnedSessions({
|
|
279
|
-
sessions,
|
|
280
|
-
recordedSessionIds: recorded.ids,
|
|
281
|
-
projectScope: projectRoot
|
|
282
|
-
});
|
|
283
|
-
const notices = [...recorded.notices, ...compared.notices];
|
|
284
|
-
if (ledger.notice) notices.push(ledger.notice);
|
|
285
|
-
const orphaned = [];
|
|
286
|
-
if (ledger.safe && recorded.safe) for (const session of compared.orphaned) {
|
|
287
|
-
const claim = ledger.claims.get(session.id);
|
|
288
|
-
if (!claim || claim.name !== session.name || claim.platform !== session.platform) {
|
|
289
|
-
notices.push(`EAS session ${session.id} (${session.name}) is unclaimed or its fixed ownership record does not match; it was not selected for deletion.`);
|
|
290
|
-
continue;
|
|
291
|
-
}
|
|
292
|
-
const state = claimStateAbsence(claim);
|
|
293
|
-
if (state.notice) {
|
|
294
|
-
notices.push(state.notice);
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
if (state.absent) orphaned.push(session);
|
|
298
|
-
}
|
|
299
|
-
return {
|
|
300
|
-
projectScope: projectRoot,
|
|
301
|
-
orphaned,
|
|
302
|
-
notices,
|
|
303
|
-
deletionSafe: recorded.safe && ledger.safe
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
function easLockRoots(projectRoot, config) {
|
|
307
|
-
return [...new Set([...Object.keys(config?.projects ?? {}), projectRoot].map(canonicalPath))].toSorted();
|
|
308
|
-
}
|
|
309
|
-
function unavailableLockRootNotice(roots) {
|
|
310
|
-
for (const root of roots) try {
|
|
311
|
-
if (!statSync$1(root).isDirectory()) throw new Error("path is not a directory");
|
|
312
|
-
readdirSync$1(root);
|
|
313
|
-
} catch (error) {
|
|
314
|
-
return `EAS session sweep skipped: ${root} is not available for remote-session lock inspection: ${describeError(error)}`;
|
|
315
|
-
}
|
|
316
|
-
return null;
|
|
317
|
-
}
|
|
318
|
-
function malformedRemoteSessionLockNotice(roots) {
|
|
319
|
-
for (const root of roots) {
|
|
320
|
-
const lockPath = join$1(workspaceDir(root), "remote-session.lock");
|
|
321
|
-
if (!existsSync$1(lockPath)) continue;
|
|
322
|
-
if ((0, ownership_claim_exports.readClaimSet)(lockPath).unresolved.length === 0) continue;
|
|
323
|
-
return `EAS session sweep skipped: the remote-session lock at ${lockPath} has a malformed owner record.`;
|
|
324
|
-
}
|
|
325
|
-
return null;
|
|
326
|
-
}
|
|
327
|
-
async function withRemoteSessionGcLocks(projectRoot, deps, fn) {
|
|
328
|
-
const withLock = deps.withRemoteSessionLock ?? withRemoteSessionLock;
|
|
329
|
-
const waitMs = deps.easLockWaitMs ?? EAS_LOCK_WAIT_MS;
|
|
330
|
-
const maxSnapshots = deps.easLockSnapshotRetries ?? EAS_LOCK_SNAPSHOT_RETRIES;
|
|
331
|
-
for (let attempt = 0; attempt < maxSnapshots; attempt++) {
|
|
332
|
-
const roots = easLockRoots(projectRoot, loadConfig$1());
|
|
333
|
-
const unavailable = unavailableLockRootNotice(roots);
|
|
334
|
-
if (unavailable) return fn({
|
|
335
|
-
...deps,
|
|
336
|
-
easSweepBlockedNotice: unavailable
|
|
337
|
-
});
|
|
338
|
-
const malformed = malformedRemoteSessionLockNotice(roots);
|
|
339
|
-
if (malformed) return fn({
|
|
340
|
-
...deps,
|
|
341
|
-
easSweepBlockedNotice: malformed
|
|
342
|
-
});
|
|
343
|
-
let expanded = false;
|
|
344
|
-
let coreStarted = false;
|
|
345
|
-
const acquire = (index) => index === roots.length ? (async () => {
|
|
346
|
-
if (easLockRoots(projectRoot, loadConfig$1()).some((root) => !roots.includes(root))) {
|
|
347
|
-
expanded = true;
|
|
348
|
-
return null;
|
|
349
|
-
}
|
|
350
|
-
coreStarted = true;
|
|
351
|
-
return fn({
|
|
352
|
-
...deps,
|
|
353
|
-
easSweepBlockedNotice: void 0,
|
|
354
|
-
easLockedRoots: roots
|
|
355
|
-
});
|
|
356
|
-
})() : withLock(roots[index], () => acquire(index + 1), { waitMs });
|
|
357
|
-
try {
|
|
358
|
-
const result = await acquire(0);
|
|
359
|
-
if (!expanded) return result;
|
|
360
|
-
} catch (error) {
|
|
361
|
-
if (coreStarted) throw error;
|
|
362
|
-
return fn({
|
|
363
|
-
...deps,
|
|
364
|
-
easSweepBlockedNotice: `EAS session sweep skipped: remote-session lock acquisition failed: ${describeError(error)}`
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
return fn({
|
|
369
|
-
...deps,
|
|
370
|
-
easSweepBlockedNotice: `EAS session sweep skipped: registered workspace roots kept changing during remote-session lock acquisition.`
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
async function deleteEasSessions(easSessionSweep, deps) {
|
|
374
|
-
let deleteFailures = 0;
|
|
375
|
-
const refuseAll = (detail) => {
|
|
376
|
-
deleteFailures += easSessionSweep.orphaned.length;
|
|
377
|
-
for (const session of easSessionSweep.orphaned) recordGcResult("easSession", "failed", session.name, {
|
|
378
|
-
id: session.id,
|
|
379
|
-
detail
|
|
380
|
-
});
|
|
381
|
-
};
|
|
382
|
-
if (easSessionSweep.orphaned.length && easSessionSweep.deletionSafe && easSessionSweep.projectScope) {
|
|
383
|
-
const projectScope = easSessionSweep.projectScope;
|
|
384
|
-
const withProjectLock = deps.withEasProjectLock ?? withEasProjectLock;
|
|
385
|
-
let finalSweepStarted = false;
|
|
386
|
-
try {
|
|
387
|
-
await withProjectLock(projectScope, () => {
|
|
388
|
-
finalSweepStarted = true;
|
|
389
|
-
return withRemoteSessionGcLocks(projectScope, deps, async (lockedDeps) => {
|
|
390
|
-
if (lockedDeps.easSweepBlockedNotice) {
|
|
391
|
-
refuseAll(`EAS session deletion refused: ${lockedDeps.easSweepBlockedNotice}`);
|
|
392
|
-
console.log(chalk.red(`EAS session deletion refused: ${lockedDeps.easSweepBlockedNotice}`));
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
const coordinatedDeps = lockedDeps;
|
|
396
|
-
const currentConfig = loadConfig$1();
|
|
397
|
-
const currentRoots = easLockRoots(projectScope, currentConfig);
|
|
398
|
-
const registryExpanded = coordinatedDeps.easLockedRoots && currentRoots.some((root) => !coordinatedDeps.easLockedRoots.includes(root));
|
|
399
|
-
const currentRecords = registryExpanded ? null : readRecordedRemoteSessionIds([...Object.keys(currentConfig?.projects ?? {}), projectScope]);
|
|
400
|
-
const ledgerRoot = coordinatedDeps.easLedgerRoot ?? easMachineStateRoot$1();
|
|
401
|
-
const ledger = readEasSessionLedger$1(ledgerRoot);
|
|
402
|
-
const removeClaim = coordinatedDeps.removeEasSessionClaim ?? removeEasSessionClaim;
|
|
403
|
-
const reconcileClaim = (sessionId) => {
|
|
404
|
-
let detail = "the claim store did not remove it";
|
|
405
|
-
try {
|
|
406
|
-
if (removeClaim(sessionId, ledgerRoot)) return;
|
|
407
|
-
} catch (error) {
|
|
408
|
-
detail = describeError(error);
|
|
409
|
-
}
|
|
410
|
-
deleteFailures++;
|
|
411
|
-
console.log(chalk.red(`EAS session ${sessionId} is resolved, but its ownership claim could not be removed: ${detail}. The claim was retained for reconciliation.`));
|
|
412
|
-
};
|
|
413
|
-
if (registryExpanded) {
|
|
414
|
-
refuseAll("EAS session deletion refused: registered workspace roots changed after classification.");
|
|
415
|
-
console.log(chalk.red("EAS session deletion refused: registered workspace roots changed after classification."));
|
|
416
|
-
} else if (!currentRecords?.safe || !ledger.safe) {
|
|
417
|
-
refuseAll("EAS session deletion refused: the session records could not be read safely.");
|
|
418
|
-
for (const notice of [...currentRecords?.notices ?? [], ...ledger.notice ? [ledger.notice] : []]) console.log(chalk.red(`EAS session deletion refused: ${notice}`));
|
|
419
|
-
} else {
|
|
420
|
-
const recorded = new Set(currentRecords.ids);
|
|
421
|
-
const eas = (coordinatedDeps.resolveEasCliBin ?? resolveEasCliBin)(projectScope);
|
|
422
|
-
const run = coordinatedDeps.runEasFile ?? ((file, args, options) => getExecutor().runFile(file, args, options));
|
|
423
|
-
for (const session of easSessionSweep.orphaned) {
|
|
424
|
-
if (recorded.has(session.id)) {
|
|
425
|
-
console.log(chalk.dim(`EAS session ${session.id} has a workspace record and was left running.`));
|
|
426
|
-
recordGcResult("easSession", "kept", session.name, {
|
|
427
|
-
id: session.id,
|
|
428
|
-
detail: "it has a workspace record and was left running"
|
|
429
|
-
});
|
|
430
|
-
continue;
|
|
431
|
-
}
|
|
432
|
-
const claim = ledger.claims.get(session.id);
|
|
433
|
-
const claimState = claim ? claimStateAbsence(claim) : null;
|
|
434
|
-
if (!claim || claim.name !== session.name || claim.platform !== session.platform || !claimState?.absent || claimState.notice) {
|
|
435
|
-
deleteFailures++;
|
|
436
|
-
console.log(chalk.red(`Could not verify fixed ownership for EAS session ${session.id}: ${claimState?.notice ?? "the claim is missing, mismatched, or still recorded in workspace state"}.`));
|
|
437
|
-
recordGcResult("easSession", "failed", session.name, {
|
|
438
|
-
id: session.id,
|
|
439
|
-
detail: `Could not verify fixed ownership for EAS session ${session.id}: ${claimState?.notice ?? "the claim is missing, mismatched, or still recorded in workspace state"}.`
|
|
440
|
-
});
|
|
441
|
-
continue;
|
|
442
|
-
}
|
|
443
|
-
if (!eas) {
|
|
444
|
-
deleteFailures++;
|
|
445
|
-
console.log(chalk.red(`Could not verify EAS session ${session.id}: eas-cli is not available.`));
|
|
446
|
-
recordGcResult("easSession", "failed", session.name, {
|
|
447
|
-
id: session.id,
|
|
448
|
-
detail: `Could not verify EAS session ${session.id}: eas-cli is not available.`
|
|
449
|
-
});
|
|
450
|
-
continue;
|
|
451
|
-
}
|
|
452
|
-
const options = {
|
|
453
|
-
cwd: session.projectScope,
|
|
454
|
-
timeoutMs: EAS_OPERATION_TIMEOUT_MS,
|
|
455
|
-
omitEnv: PROXY_CREDENTIAL_ENV
|
|
456
|
-
};
|
|
457
|
-
let live;
|
|
458
|
-
try {
|
|
459
|
-
live = run(eas.file, getSessionArgs(session.id), options);
|
|
460
|
-
} catch (error) {
|
|
461
|
-
if (isDefinitiveMissingSessionError(error, session.id)) {
|
|
462
|
-
console.log(chalk.dim(`EAS session ${session.id} is already gone.`));
|
|
463
|
-
reconcileClaim(session.id);
|
|
464
|
-
} else {
|
|
465
|
-
deleteFailures++;
|
|
466
|
-
console.log(chalk.red(`Could not verify EAS session ${session.id}: ${describeError(error)}`));
|
|
467
|
-
recordGcResult("easSession", "failed", session.name, {
|
|
468
|
-
id: session.id,
|
|
469
|
-
detail: `Could not verify EAS session ${session.id}: ${describeError(error)}`
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
continue;
|
|
473
|
-
}
|
|
474
|
-
const inspection = inspectSessionForTeardown(live, session.id);
|
|
475
|
-
if (inspection.action === "already-stopped") {
|
|
476
|
-
console.log(chalk.dim(`EAS session ${session.id} is already stopped (${inspection.status}).`));
|
|
477
|
-
reconcileClaim(session.id);
|
|
478
|
-
continue;
|
|
479
|
-
}
|
|
480
|
-
if (inspection.action === "refused") {
|
|
481
|
-
deleteFailures++;
|
|
482
|
-
console.log(chalk.red(`Could not stop EAS session ${session.id}: ${inspection.reason}`));
|
|
483
|
-
recordGcResult("easSession", "failed", session.name, {
|
|
484
|
-
id: session.id,
|
|
485
|
-
detail: `Could not stop EAS session ${session.id}: ${inspection.reason}`
|
|
486
|
-
});
|
|
487
|
-
continue;
|
|
488
|
-
}
|
|
489
|
-
let stopped;
|
|
490
|
-
try {
|
|
491
|
-
stopped = run(eas.file, stopSessionArgs(session.id), options);
|
|
492
|
-
} catch (error) {
|
|
493
|
-
deleteFailures++;
|
|
494
|
-
console.log(chalk.red(`Could not stop EAS session ${session.id}: ${describeError(error)}`));
|
|
495
|
-
recordGcResult("easSession", "failed", session.name, {
|
|
496
|
-
id: session.id,
|
|
497
|
-
detail: `Could not stop EAS session ${session.id}: ${describeError(error)}`
|
|
498
|
-
});
|
|
499
|
-
continue;
|
|
500
|
-
}
|
|
501
|
-
const verified = verifyStoppedSession(stopped, session.id);
|
|
502
|
-
if (!verified.ok) {
|
|
503
|
-
deleteFailures++;
|
|
504
|
-
console.log(chalk.red(`Could not verify EAS session ${session.id} stopped: ${verified.reason}`));
|
|
505
|
-
recordGcResult("easSession", "failed", session.name, {
|
|
506
|
-
id: session.id,
|
|
507
|
-
detail: `Could not verify EAS session ${session.id} stopped: ${verified.reason}`
|
|
508
|
-
});
|
|
509
|
-
continue;
|
|
510
|
-
}
|
|
511
|
-
console.log(chalk.green(`Stopped EAS session ${session.id} (${session.name})`));
|
|
512
|
-
recordGcResult("easSession", "done", session.name, { id: session.id });
|
|
513
|
-
reconcileClaim(session.id);
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
});
|
|
517
|
-
}, {
|
|
518
|
-
waitMs: 0,
|
|
519
|
-
ownerPurpose: "EAS orphan deletion",
|
|
520
|
-
machineRoot: deps.easLedgerRoot
|
|
521
|
-
});
|
|
522
|
-
} catch (error) {
|
|
523
|
-
const phase = finalSweepStarted ? "failed" : "lock acquisition failed";
|
|
524
|
-
refuseAll(`EAS session deletion ${phase}: ${describeError(error)}`);
|
|
525
|
-
console.log(chalk.red(`EAS session deletion ${phase}: ${describeError(error)}`));
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
return deleteFailures;
|
|
529
|
-
}
|
|
530
|
-
//#endregion
|
|
531
121
|
//#region src/commands/gc/eas-ledger.ts
|
|
532
122
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
533
123
|
/**
|
|
@@ -548,20 +138,92 @@ function findStaleEasLedgerEntries(mountedVolumes, deps = {}) {
|
|
|
548
138
|
workspaceRoot
|
|
549
139
|
}));
|
|
550
140
|
}
|
|
551
|
-
function forgetStaleEasLedgerEntries(entries, mountedVolumes, deps = {}) {
|
|
141
|
+
async function forgetStaleEasLedgerEntries(entries, mountedVolumes, deps = {}, output = GC_OUTPUT, signal) {
|
|
142
|
+
signal?.throwIfAborted();
|
|
552
143
|
let failures = 0;
|
|
553
144
|
const root = deps.easLedgerRoot ?? easMachineStateRoot$1();
|
|
554
|
-
const remove = deps.removeEasSessionClaim ??
|
|
555
|
-
for (const entry of entries)
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
145
|
+
const remove = deps.removeEasSessionClaim ?? removeEasSessionClaimAsync;
|
|
146
|
+
for (const entry of entries) {
|
|
147
|
+
await setImmediate();
|
|
148
|
+
signal?.throwIfAborted();
|
|
149
|
+
try {
|
|
150
|
+
if (await remove(entry.id, root, (claim) => isStaleEasLedgerEntry(claim, mountedVolumes), signal)) output.log(chalk.green(`Forgot the EAS ledger entry ${entry.id} (invalid id, ${entry.workspaceRoot} is gone)`));
|
|
151
|
+
else output.log(chalk.dim(`Kept the EAS ledger entry ${entry.id}: it changed since the report.`));
|
|
152
|
+
} catch (error) {
|
|
153
|
+
signal?.throwIfAborted();
|
|
154
|
+
failures++;
|
|
155
|
+
output.log(chalk.red(`Could not forget the EAS ledger entry ${entry.id}: ${describeError(error)}`));
|
|
156
|
+
}
|
|
561
157
|
}
|
|
158
|
+
signal?.throwIfAborted();
|
|
562
159
|
return failures;
|
|
563
160
|
}
|
|
564
161
|
//#endregion
|
|
162
|
+
//#region src/devices/machine-cache-usage-output.ts
|
|
163
|
+
const size = (bytes) => bytes === null ? "unknown" : formatBytes(bytes);
|
|
164
|
+
function total(values) {
|
|
165
|
+
const known = values.reduce((sum, value) => sum + (value ?? 0), 0);
|
|
166
|
+
return `${values.some((value) => value === null) ? "at least " : ""}${formatBytes(known)}`;
|
|
167
|
+
}
|
|
168
|
+
function machineCacheLines(report, now = Date.now()) {
|
|
169
|
+
if (!report) return [];
|
|
170
|
+
const { gradle, simRuntimes, deviceSupport, runnerBuilds } = report;
|
|
171
|
+
const body = [];
|
|
172
|
+
if (gradle.unusedVersions.length) {
|
|
173
|
+
body.push(` Gradle versions no registered project pins (${total(gradle.unusedVersions.map((entry) => entry.bytes))}):`);
|
|
174
|
+
for (const entry of gradle.unusedVersions) {
|
|
175
|
+
body.push(` ${entry.version}: ${size(entry.bytes)}`);
|
|
176
|
+
for (const dir of entry.dirs) body.push(` ${dir.dir}`);
|
|
177
|
+
if (entry.remedy) body.push(chalk.dim(` remedy: ${entry.remedy}`));
|
|
178
|
+
}
|
|
179
|
+
if (gradle.unreadableProjects.length) body.push(chalk.dim(` ${gradle.unreadableProjects.length} registered project${gradle.unreadableProjects.length === 1 ? " is" : "s are"} missing or unreadable, so a version listed here may still be in use: ${gradle.unreadableProjects.join(", ")}`));
|
|
180
|
+
}
|
|
181
|
+
if (gradle.modules) {
|
|
182
|
+
body.push(` Gradle dependency cache: ${size(gradle.modules.bytes)}`, ` ${gradle.modules.dir}`);
|
|
183
|
+
body.push(chalk.dim(` remedy: ${gradle.modules.remedy}`));
|
|
184
|
+
}
|
|
185
|
+
if (!simRuntimes.checked) body.push(chalk.dim(" Simulator runtimes: not checked, simctl did not answer"));
|
|
186
|
+
else if (simRuntimes.runtimes.length) {
|
|
187
|
+
body.push(` Simulator runtimes (${total(simRuntimes.runtimes.map((entry) => entry.bytes))}):`);
|
|
188
|
+
for (const runtime of simRuntimes.runtimes) {
|
|
189
|
+
const details = [
|
|
190
|
+
runtime.devices ? `${runtime.devices} simulator${runtime.devices === 1 ? "" : "s"}` : "no simulator uses it",
|
|
191
|
+
runtime.lastUsedAt ? `last used ${runtime.lastUsedAt.slice(0, 10)}` : null,
|
|
192
|
+
runtime.deletable ? null : "not deletable"
|
|
193
|
+
].filter(Boolean);
|
|
194
|
+
body.push(` ${runtime.runtimeIdentifier.replace(/^com\.apple\.CoreSimulator\.SimRuntime\./, "")}${runtime.build ? ` (${runtime.build})` : ""}: ${size(runtime.bytes)}, ${details.join(", ")}`);
|
|
195
|
+
if (runtime.remedy) body.push(chalk.dim(` remedy: ${runtime.remedy}`));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (deviceSupport.entries.length) {
|
|
199
|
+
body.push(` Xcode device support (${total(deviceSupport.entries.map((entry) => entry.bytes))}):`);
|
|
200
|
+
for (const entry of deviceSupport.entries) body.push(` ${entry.dir}: ${size(entry.bytes)}`);
|
|
201
|
+
body.push(chalk.dim(` remedy: ${deviceSupport.remedy}`));
|
|
202
|
+
}
|
|
203
|
+
if (runnerBuilds.groups.length) {
|
|
204
|
+
const currentVersions = runnerBuilds.agentDeviceVersion && runnerBuilds.xcodeBuildVersion ? `current: agent-device ${runnerBuilds.agentDeviceVersion}, Xcode ${runnerBuilds.xcodeBuildVersion}` : "current versions unknown";
|
|
205
|
+
body.push(` agent-device runner builds by version (${currentVersions}):`);
|
|
206
|
+
for (const group of runnerBuilds.groups) {
|
|
207
|
+
const details = [
|
|
208
|
+
`${group.entries} entr${group.entries === 1 ? "y" : "ies"}`,
|
|
209
|
+
size(group.bytes),
|
|
210
|
+
group.oldestUseAt ? `oldest use ${group.oldestUseAt.slice(0, 10)}` : null,
|
|
211
|
+
group.inUse ? `${group.inUse} in use` : null,
|
|
212
|
+
group.current ? "current" : group.current === false ? "older toolchain" : null
|
|
213
|
+
].filter(Boolean);
|
|
214
|
+
body.push(` agent-device ${group.packageVersion ?? "unknown"}, Xcode ${group.xcodeBuildVersion ?? "unknown"}: ${details.join(", ")}`);
|
|
215
|
+
}
|
|
216
|
+
body.push(chalk.dim(` remedy: ${runnerBuilds.remedy}`));
|
|
217
|
+
}
|
|
218
|
+
for (const notice of report.notices) body.push(chalk.dim(` ${notice}`));
|
|
219
|
+
if (!body.length) return [];
|
|
220
|
+
return [
|
|
221
|
+
`Other tools' machine-wide caches${report.complete ? "" : " (incomplete)"} - measured ${formatLongDuration(Math.max(0, now - Date.parse(report.measuredAt)))} ago`,
|
|
222
|
+
...body,
|
|
223
|
+
chalk.dim(" Stim reports these and never deletes them; use each tool's own cleanup.")
|
|
224
|
+
];
|
|
225
|
+
}
|
|
226
|
+
//#endregion
|
|
565
227
|
//#region src/commands/gc/status-caches.ts
|
|
566
228
|
const KINDS = ["disk-usage", "pull-request"];
|
|
567
229
|
const KEPT_REASON = {
|
|
@@ -627,23 +289,26 @@ function statusCacheLines(stale) {
|
|
|
627
289
|
lines.push(" --delete removes them; `stim status` measures a folder again if it comes back.");
|
|
628
290
|
return lines;
|
|
629
291
|
}
|
|
630
|
-
function deleteStaleStatusCaches(stale) {
|
|
292
|
+
async function deleteStaleStatusCaches(stale, output = GC_OUTPUT, signal) {
|
|
293
|
+
signal?.throwIfAborted();
|
|
631
294
|
const mountedVolumes = listMountedVolumes();
|
|
632
295
|
let failures = 0;
|
|
633
296
|
for (const kind of KINDS) {
|
|
634
297
|
let removed = 0;
|
|
635
298
|
let bytes = 0;
|
|
636
299
|
for (const entry of stale.filter((candidate) => candidate.kind === kind)) {
|
|
300
|
+
signal?.throwIfAborted();
|
|
637
301
|
if (judge(entry.path, mountedVolumes) !== "gone") continue;
|
|
638
302
|
try {
|
|
639
|
-
|
|
303
|
+
await rm(entry.file, { force: true });
|
|
640
304
|
removed++;
|
|
641
305
|
bytes += entry.bytes;
|
|
642
306
|
} catch (error) {
|
|
307
|
+
signal?.throwIfAborted();
|
|
643
308
|
failures++;
|
|
644
309
|
const detail = error?.message || String(error);
|
|
645
|
-
|
|
646
|
-
|
|
310
|
+
output.log(chalk.red(`Could not remove ${entry.file}: ${detail}`));
|
|
311
|
+
output.record("statusCache", "failed", `${kind} cache entry`, {
|
|
647
312
|
id: entry.file,
|
|
648
313
|
detail
|
|
649
314
|
});
|
|
@@ -651,11 +316,90 @@ function deleteStaleStatusCaches(stale) {
|
|
|
651
316
|
}
|
|
652
317
|
if (!removed) continue;
|
|
653
318
|
const label = `${removed} stale ${kind} cache entr${removed === 1 ? "y" : "ies"}`;
|
|
654
|
-
|
|
655
|
-
|
|
319
|
+
output.log(chalk.green(`Removed ${label}`));
|
|
320
|
+
output.record("statusCache", "done", label, { bytes });
|
|
321
|
+
}
|
|
322
|
+
signal?.throwIfAborted();
|
|
323
|
+
return failures;
|
|
324
|
+
}
|
|
325
|
+
//#endregion
|
|
326
|
+
//#region src/commands/gc/finished-devices.ts
|
|
327
|
+
/** The owned devices of workspaces whose linked worktree is finished but kept, such as a dirty one. */
|
|
328
|
+
function collectFinishedDevices({ config, sims, avds, sweep, now = Date.now() }) {
|
|
329
|
+
const keys = Object.keys(config?.projects ?? {});
|
|
330
|
+
const devices = [];
|
|
331
|
+
const seen = /* @__PURE__ */ new Set();
|
|
332
|
+
for (const { root, reason } of finishedWorkspaces(sweep, now)) {
|
|
333
|
+
const key = keys.find((candidate) => canonicalPath(candidate) === root);
|
|
334
|
+
const project = key ? config?.projects[key] : void 0;
|
|
335
|
+
if (!config || !key || !project || seen.has(key)) continue;
|
|
336
|
+
seen.add(key);
|
|
337
|
+
for (const { idleDays: _idleDays, ...device } of findStaleProjectDevices({
|
|
338
|
+
config: {
|
|
339
|
+
...config,
|
|
340
|
+
projects: { [key]: project }
|
|
341
|
+
},
|
|
342
|
+
sims,
|
|
343
|
+
avds,
|
|
344
|
+
olderThanDays: 0,
|
|
345
|
+
lastTouched: () => 0
|
|
346
|
+
})) devices.push({
|
|
347
|
+
...device,
|
|
348
|
+
reason
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
return devices;
|
|
352
|
+
}
|
|
353
|
+
function finishedDeviceLines(devices = []) {
|
|
354
|
+
if (!devices.length) return [];
|
|
355
|
+
const lines = [`Owned devices of finished worktrees (${devices.length}) - the worktree is kept, its devices are not:`];
|
|
356
|
+
for (const device of devices) {
|
|
357
|
+
lines.push(` ${device.kind} ${device.name} (${device.id})${device.bytes ? ` ${formatBytes(device.bytes)}` : ""}`);
|
|
358
|
+
lines.push(` ${device.project}: ${device.reason}`);
|
|
359
|
+
}
|
|
360
|
+
lines.push(" --delete keeps a workspace that is in use, leased, driven, locked or pinned.");
|
|
361
|
+
return lines;
|
|
362
|
+
}
|
|
363
|
+
async function deleteFinishedDevices({ finishedDevices: devices, worktreeSweep }, output = GC_OUTPUT, operation) {
|
|
364
|
+
const graceMs = worktreeSweep?.graceMs ?? 0;
|
|
365
|
+
let failures = 0;
|
|
366
|
+
for (const project of new Set(devices.map((device) => device.project))) {
|
|
367
|
+
operation?.signal.throwIfAborted();
|
|
368
|
+
const outcome = await deleteIdleWorkspaceDevices(canonicalPath(project), {
|
|
369
|
+
windowMs: graceMs,
|
|
370
|
+
protectedRoot: triggeringRoot(operation),
|
|
371
|
+
purpose: "stim gc --delete",
|
|
372
|
+
operation,
|
|
373
|
+
output
|
|
374
|
+
});
|
|
375
|
+
if (outcome.status === "kept") {
|
|
376
|
+
output.log(chalk.yellow(`Kept the owned devices of ${project}: ${outcome.detail}`));
|
|
377
|
+
output.record("device", "kept", project, { detail: outcome.detail });
|
|
378
|
+
} else if (outcome.status === "failed") {
|
|
379
|
+
failures++;
|
|
380
|
+
output.log(chalk.red(`Could not delete the owned devices of ${project}: ${outcome.detail}`));
|
|
381
|
+
output.record("device", "failed", project, { detail: outcome.detail });
|
|
382
|
+
}
|
|
656
383
|
}
|
|
657
384
|
return failures;
|
|
658
385
|
}
|
|
386
|
+
/** Owned simulator and emulator disk of registered workspaces, by platform. */
|
|
387
|
+
function ownedDeviceDisk(usage, unmeasured = {}) {
|
|
388
|
+
return ["ios", "android"].flatMap((kind) => {
|
|
389
|
+
const owned = usage.filter((device) => device.kind === kind && device.workspace);
|
|
390
|
+
const missing = unmeasured[kind] ?? 0;
|
|
391
|
+
return owned.length || missing ? [{
|
|
392
|
+
kind,
|
|
393
|
+
count: owned.length,
|
|
394
|
+
bytes: owned.reduce((sum, device) => sum + device.bytes, 0),
|
|
395
|
+
unmeasured: missing
|
|
396
|
+
}] : [];
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
function ownedDeviceDiskLines(disk = []) {
|
|
400
|
+
if (!disk.length) return [];
|
|
401
|
+
return [`Owned device disk: ${disk.map(({ kind, count, bytes, unmeasured }) => `${count} ${kind === "ios" ? "simulators" : "emulators"} ${formatBytes(bytes)}${unmeasured ? ` (${unmeasured} not measured)` : ""}`).join(", ")}`];
|
|
402
|
+
}
|
|
659
403
|
//#endregion
|
|
660
404
|
//#region src/commands/gc/recordings.ts
|
|
661
405
|
const DAY_MS = 864e5;
|
|
@@ -702,23 +446,37 @@ function planRecordings(entries, { whole, olderThan, now }) {
|
|
|
702
446
|
* workspace and dead project sweeps remove whole, are listed with them; entries whose project root is on an
|
|
703
447
|
* unmounted volume are left out.
|
|
704
448
|
*/
|
|
705
|
-
function collectRecordings(plan, gone = []) {
|
|
449
|
+
async function collectRecordings(plan, gone = [], signal) {
|
|
450
|
+
signal?.throwIfAborted();
|
|
706
451
|
const mountedVolumes = listMountedVolumes();
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
452
|
+
const entries = [];
|
|
453
|
+
for (const entry of listWorkspaceDirs()) {
|
|
454
|
+
signal?.throwIfAborted();
|
|
455
|
+
if (entry.projectRoot !== null && !isOnMountedVolume(entry.projectRoot, mountedVolumes)) continue;
|
|
456
|
+
const recordings = join$1(entry.dir, "recordings");
|
|
457
|
+
if (!existsSync$1(recordings)) continue;
|
|
458
|
+
entries.push({
|
|
459
|
+
...entry,
|
|
460
|
+
recordings,
|
|
461
|
+
gone: gone.includes(entry.dir),
|
|
462
|
+
devices: await listRecordedDevicesAsync(recordings, false, signal)
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
signal?.throwIfAborted();
|
|
466
|
+
return planRecordings(entries, plan);
|
|
711
467
|
}
|
|
712
468
|
/** Deletes what `planRecordings` chose, planning again from the files on disk. */
|
|
713
|
-
function deleteRecordings(report, plan) {
|
|
469
|
+
async function deleteRecordings(report, plan, output = GC_OUTPUT, signal) {
|
|
470
|
+
signal?.throwIfAborted();
|
|
714
471
|
let failures = 0;
|
|
715
472
|
for (const entry of report) {
|
|
473
|
+
signal?.throwIfAborted();
|
|
716
474
|
const label = entry.projectRoot ?? entry.dir;
|
|
717
475
|
if (entry.withWorkspace) continue;
|
|
718
476
|
if (!entry.willDelete) {
|
|
719
477
|
if (entry.keptCode === "unresolved" || entry.keptCode === "recently-recorded") {
|
|
720
|
-
|
|
721
|
-
|
|
478
|
+
output.log(chalk.dim(`Kept the device recordings of ${label}: ${entry.keptReason}`));
|
|
479
|
+
output.record("recording", "kept", label, {
|
|
722
480
|
bytes: entry.bytes,
|
|
723
481
|
detail: entry.keptReason
|
|
724
482
|
});
|
|
@@ -728,27 +486,34 @@ function deleteRecordings(report, plan) {
|
|
|
728
486
|
try {
|
|
729
487
|
let deleted = 0;
|
|
730
488
|
if (plan.whole) {
|
|
731
|
-
deleted = recordedBytes(
|
|
732
|
-
|
|
489
|
+
deleted = recordedBytes(await listRecordedDevicesAsync(entry.dir, false, signal));
|
|
490
|
+
signal?.throwIfAborted();
|
|
491
|
+
await rm(entry.dir, {
|
|
733
492
|
recursive: true,
|
|
734
493
|
force: true
|
|
735
494
|
});
|
|
736
|
-
} else if (plan.olderThan !== null)
|
|
737
|
-
|
|
738
|
-
|
|
495
|
+
} else if (plan.olderThan !== null) {
|
|
496
|
+
const devices = await listRecordedDevicesAsync(entry.dir, false, signal);
|
|
497
|
+
for (const segment of oldSegments(devices, plan.olderThan, plan.now)) {
|
|
498
|
+
signal?.throwIfAborted();
|
|
499
|
+
await rm(segment.file, { force: true });
|
|
500
|
+
deleted += segment.bytes;
|
|
501
|
+
}
|
|
739
502
|
}
|
|
740
|
-
|
|
741
|
-
|
|
503
|
+
output.log(chalk.green(`Deleted ${formatBytes(deleted)} of device recordings of ${label}`));
|
|
504
|
+
output.record("recording", "done", label, { bytes: deleted });
|
|
742
505
|
} catch (error) {
|
|
506
|
+
signal?.throwIfAborted();
|
|
743
507
|
failures++;
|
|
744
508
|
const detail = error?.message || String(error);
|
|
745
|
-
|
|
746
|
-
|
|
509
|
+
output.log(chalk.red(`Could not delete the device recordings of ${label}: ${detail}`));
|
|
510
|
+
output.record("recording", "failed", label, {
|
|
747
511
|
bytes: entry.bytes,
|
|
748
512
|
detail
|
|
749
513
|
});
|
|
750
514
|
}
|
|
751
515
|
}
|
|
516
|
+
signal?.throwIfAborted();
|
|
752
517
|
return failures;
|
|
753
518
|
}
|
|
754
519
|
function recordingLines(recordings) {
|
|
@@ -850,7 +615,7 @@ function staleEasLedgerLines(entries = []) {
|
|
|
850
615
|
" --delete forgets the LEDGER ENTRY only; no EAS session is contacted."
|
|
851
616
|
];
|
|
852
617
|
}
|
|
853
|
-
function formatGcReport({ maintenance, archives, skipped = [], deadProjects = [], orphanedPorts, invalidProjects = [], orphanedWorkspaces = [], parkedSims = [], parkedAvds = [], parkedHostedDevices, orphanedDevices = [], unverifiedDevices = [], staleDevices = [], staleDeviceRecords = [], staleLedgerEntries = [], staleEasLedgerEntries, staleStatusCaches = [], buildLocks = {
|
|
618
|
+
function formatGcReport({ maintenance, archives, skipped = [], deadProjects = [], orphanedPorts, invalidProjects = [], orphanedWorkspaces = [], parkedSims = [], parkedAvds = [], parkedHostedDevices, orphanedDevices = [], unverifiedDevices = [], staleDevices = [], finishedDevices, ownedDeviceDisk, staleDeviceRecords = [], staleLedgerEntries = [], staleEasLedgerEntries, staleStatusCaches = [], buildLocks = {
|
|
854
619
|
stale: [],
|
|
855
620
|
live: [],
|
|
856
621
|
unresolved: []
|
|
@@ -866,10 +631,11 @@ function formatGcReport({ maintenance, archives, skipped = [], deadProjects = []
|
|
|
866
631
|
orphaned: [],
|
|
867
632
|
notices: [],
|
|
868
633
|
deletionSafe: true
|
|
869
|
-
}, agentDeviceUsage = null, swiftpmCacheUsage, caches = [], workspaceOutputs = null, workspaceLogs = [], recordings = [], worktreeSweep = null, memory, cacheScope = null, olderThan = null }, { now = Date.now() } = {}) {
|
|
634
|
+
}, agentDeviceUsage = null, swiftpmCacheUsage, machineCaches, tempLeftovers, gradleVersions, caches = [], workspaceOutputs = null, workspaceLogs = [], recordings = [], worktreeSweep = null, memory, cacheScope = null, olderThan = null }, { now = Date.now() } = {}) {
|
|
870
635
|
const lines = archiveLines(archives);
|
|
871
636
|
const parkedHostedLines = formatParkedHostedDeviceReport(parkedHostedDevices, now);
|
|
872
637
|
const easLedgerLines = staleEasLedgerLines(staleEasLedgerEntries);
|
|
638
|
+
const finishedLines = finishedDeviceLines(finishedDevices);
|
|
873
639
|
const staleLocks = buildLocks?.stale ?? [];
|
|
874
640
|
const liveLocks = buildLocks?.live ?? [];
|
|
875
641
|
const staleSlots = buildSlots?.stale ?? [];
|
|
@@ -884,6 +650,7 @@ function formatGcReport({ maintenance, archives, skipped = [], deadProjects = []
|
|
|
884
650
|
parkedHostedLines,
|
|
885
651
|
orphanedDevices,
|
|
886
652
|
staleDevices,
|
|
653
|
+
finishedLines,
|
|
887
654
|
staleDeviceRecords,
|
|
888
655
|
staleLedgerEntries,
|
|
889
656
|
easLedgerLines,
|
|
@@ -908,6 +675,7 @@ function formatGcReport({ maintenance, archives, skipped = [], deadProjects = []
|
|
|
908
675
|
lines.push(...namedPortLines(orphanedPorts));
|
|
909
676
|
lines.push(...orphanedWorkspaceLines(orphanedWorkspaces));
|
|
910
677
|
lines.push(...worktreeSweepLines(worktreeSweep));
|
|
678
|
+
lines.push(...unlistedWorktreeLines(worktreeSweep));
|
|
911
679
|
lines.push(...formatParkedSimReport(parkedSims, now, cacheScope !== null));
|
|
912
680
|
lines.push(...formatParkedAvdReport(parkedAvds, now, cacheScope !== null));
|
|
913
681
|
lines.push(...parkedHostedLines);
|
|
@@ -930,6 +698,7 @@ function formatGcReport({ maintenance, archives, skipped = [], deadProjects = []
|
|
|
930
698
|
lines.push(` ${d.project} (idle ${d.idleDays}d)`);
|
|
931
699
|
}
|
|
932
700
|
}
|
|
701
|
+
lines.push(...finishedLines, ...ownedDeviceDiskLines(ownedDeviceDisk));
|
|
933
702
|
lines.push(...idleDeviceLines(idleDevices));
|
|
934
703
|
if (staleDeviceRecords.length) {
|
|
935
704
|
lines.push(`Stale device records (${staleDeviceRecords.length}) - the device is gone, the project is not:`);
|
|
@@ -995,6 +764,9 @@ function formatGcReport({ maintenance, archives, skipped = [], deadProjects = []
|
|
|
995
764
|
lines.push(...cacheLines(caches, workspaceOutputs));
|
|
996
765
|
lines.push(...agentDeviceLines(agentDeviceUsage, true, now));
|
|
997
766
|
lines.push(...swiftpmCacheLines(swiftpmCacheUsage, now));
|
|
767
|
+
lines.push(...machineCacheLines(machineCaches, now));
|
|
768
|
+
lines.push(...tempLeftoverLines(tempLeftovers));
|
|
769
|
+
lines.push(...gradleVersionLines(gradleVersions));
|
|
998
770
|
lines.push(...memoryLines(memory, memoryCacheKind(cacheScope), now));
|
|
999
771
|
lines.push(...maintenanceReportLines(maintenance));
|
|
1000
772
|
return lines;
|
|
@@ -1072,18 +844,20 @@ function cacheLines(caches, workspaceOutputs) {
|
|
|
1072
844
|
if (outputs) lines.push(...workspaceOutputLines(outputs, outputBytes));
|
|
1073
845
|
for (const c of caches) {
|
|
1074
846
|
const tag = c.source ? ` (${c.source})` : "";
|
|
1075
|
-
lines.push(` ${
|
|
847
|
+
lines.push(` ${sizeText(c.bytes ?? null).padStart(10)} ${c.name}${tag}`);
|
|
1076
848
|
lines.push(` ${c.dir}`);
|
|
1077
849
|
if (c.note) lines.push(` ${c.note}`);
|
|
850
|
+
if (c.sizeError) lines.push(` ${c.sizeError}`);
|
|
1078
851
|
if (c.willEmpty) lines.push(" would be EMPTIED");
|
|
1079
852
|
else if (c.emptySkipped) lines.push(` would be left alone: ${c.emptySkipped}`);
|
|
1080
853
|
else if (c.scopedEmpty?.emptySkipped) lines.push(` --delete --cache would leave it alone: ${c.scopedEmpty.emptySkipped}`);
|
|
1081
854
|
}
|
|
1082
|
-
|
|
855
|
+
const incomplete = caches.some((c) => c.bytes === void 0) || outputs?.workspaces.some((w) => w.bytes === null);
|
|
856
|
+
lines.push(` total: ${incomplete ? "at least " : ""}${formatBytes(total)}`);
|
|
1083
857
|
const doomed = caches.filter((c) => c.willEmpty);
|
|
1084
858
|
if (doomed.length) {
|
|
1085
859
|
const doomedBytes = doomed.reduce((n, c) => n + (c.bytes ?? 0), 0);
|
|
1086
|
-
lines.push(` would empty ${doomed.length} of these (${formatBytes(doomedBytes)})`);
|
|
860
|
+
lines.push(` would empty ${doomed.length} of these (${doomed.some((c) => c.bytes === void 0) ? "at least " : ""}${formatBytes(doomedBytes)})`);
|
|
1087
861
|
}
|
|
1088
862
|
const clearing = outputs?.workspaces.filter((w) => w.willClear) ?? [];
|
|
1089
863
|
if (clearing.length) {
|
|
@@ -1093,6 +867,29 @@ function cacheLines(caches, workspaceOutputs) {
|
|
|
1093
867
|
}
|
|
1094
868
|
return lines;
|
|
1095
869
|
}
|
|
870
|
+
const BUILD_STATE_DIRS_SHOWN = 4;
|
|
871
|
+
function buildStateLine(state) {
|
|
872
|
+
if (!state?.dirs.length) return null;
|
|
873
|
+
const shown = state.dirs.slice(0, BUILD_STATE_DIRS_SHOWN).map((dir) => `${dir.path} ${sizeText(dir.bytes)}`);
|
|
874
|
+
const more = state.dirs.length > BUILD_STATE_DIRS_SHOWN ? ", ..." : "";
|
|
875
|
+
return ` in-tree build state ${sizeText(state.bytes)}: ${shown.join(", ")}${more}`;
|
|
876
|
+
}
|
|
877
|
+
function unlistedWorktreeLines(sweep) {
|
|
878
|
+
const unlisted = sweep?.unlisted ?? [];
|
|
879
|
+
if (!unlisted.length) return [];
|
|
880
|
+
const lines = [`Directories under .worktrees that git does not list as worktrees (${unlisted.length}) - reported, never removed:`];
|
|
881
|
+
for (const dir of unlisted) {
|
|
882
|
+
lines.push(` ${dir.path} (${sizeText(dir.bytes)})`);
|
|
883
|
+
lines.push(` ${unlistedWorktreeDetail(dir)}`);
|
|
884
|
+
}
|
|
885
|
+
return lines;
|
|
886
|
+
}
|
|
887
|
+
function unlistedWorktreeDetail(dir) {
|
|
888
|
+
return `git worktree list in ${dir.repository} does not show it, so gc cannot tell whether it holds work; look through it, then remove it with \`${unlistedWorktreeRemedy(dir)}\``;
|
|
889
|
+
}
|
|
890
|
+
function unlistedWorktreeRemedy(dir) {
|
|
891
|
+
return `rm -rf '${dir.path.replaceAll("'", "'\\''")}'`;
|
|
892
|
+
}
|
|
1096
893
|
function worktreeSweepLines(sweep) {
|
|
1097
894
|
if (!sweep || !sweep.idle && !sweep.worktrees.length) return [];
|
|
1098
895
|
const removable = sweep.worktrees.filter((w) => !w.skipped);
|
|
@@ -1103,8 +900,10 @@ function worktreeSweepLines(sweep) {
|
|
|
1103
900
|
for (const w of sweep.worktrees) {
|
|
1104
901
|
const age = w.idleDays === null ? "" : ` (idle ${w.idleDays}d)`;
|
|
1105
902
|
lines.push(` ${w.path}${age}`);
|
|
903
|
+
const buildState = buildStateLine(w.buildState);
|
|
904
|
+
if (buildState) lines.push(buildState);
|
|
1106
905
|
const note = worktreePullRequestNote(w);
|
|
1107
|
-
lines.push(w.skipped ? ` kept: ${w.skipped}${note && !w.skipped.includes(note) ? ` (${note})` : ""}` : ` would be REMOVED by \`stim worktree remove\`: ${worktreeRemovalReason(w)}`);
|
|
906
|
+
lines.push(w.skipped ? ` kept: ${w.skipped}${note && !w.skipped.includes(note) ? ` (${note})` : ""}` : ` would be REMOVED by \`stim worktree remove\`: ${worktreeRemovalReason(w, sweep)}`);
|
|
1108
907
|
}
|
|
1109
908
|
if (removable.length) lines.push(" --delete runs it without --force; use, idleness and HEAD are re-checked under its removal locks.");
|
|
1110
909
|
return lines;
|
|
@@ -1127,7 +926,22 @@ function jsonPullRequest(lookup) {
|
|
|
1127
926
|
containsHead: pr.containsHead
|
|
1128
927
|
} : null;
|
|
1129
928
|
}
|
|
1130
|
-
function
|
|
929
|
+
function linkedWorktreesJson(sweep) {
|
|
930
|
+
if (!sweep) return [];
|
|
931
|
+
return sweep.worktrees.map((w) => ({
|
|
932
|
+
path: w.path,
|
|
933
|
+
idleDays: w.idleDays,
|
|
934
|
+
mergedInto: w.merge?.merged ? w.merge.into : null,
|
|
935
|
+
pullRequest: jsonPullRequest(w.pullRequest),
|
|
936
|
+
pullRequestUnknown: w.pullRequest && "unavailable" in w.pullRequest ? w.pullRequest.unavailable : null,
|
|
937
|
+
willRemove: w.skipCode === null,
|
|
938
|
+
reason: w.skipCode,
|
|
939
|
+
detail: w.skipped ?? worktreeRemovalReason(w, sweep),
|
|
940
|
+
eligibleAt: w.eligibleAt === null ? null : new Date(w.eligibleAt).toISOString(),
|
|
941
|
+
buildState: w.buildState ?? null
|
|
942
|
+
}));
|
|
943
|
+
}
|
|
944
|
+
function gcReportSections({ archives, skipped = [], deadProjects = [], orphanedPorts = [], invalidProjects = [], orphanedWorkspaces = [], worktreeSweep = null, parkedSims = [], parkedAvds = [], parkedHostedDevices = [], orphanedDevices = [], unverifiedDevices = [], staleDevices = [], finishedDevices = [], ownedDeviceDisk = [], staleDeviceRecords = [], staleLedgerEntries = [], staleEasLedgerEntries = [], staleStatusCaches = [], buildLocks = {
|
|
1131
945
|
stale: [],
|
|
1132
946
|
live: [],
|
|
1133
947
|
unresolved: []
|
|
@@ -1143,7 +957,7 @@ function gcReportSections({ archives, skipped = [], deadProjects = [], orphanedP
|
|
|
1143
957
|
orphaned: [],
|
|
1144
958
|
notices: [],
|
|
1145
959
|
deletionSafe: true
|
|
1146
|
-
}, workspaceOutputs = null, workspaceLogs = [], recordings = [], agentDeviceUsage = null, swiftpmCacheUsage = null, caches = [], memory = null }) {
|
|
960
|
+
}, workspaceOutputs = null, workspaceLogs = [], recordings = [], agentDeviceUsage = null, swiftpmCacheUsage = null, machineCaches = null, tempLeftovers = null, gradleVersions = null, caches = [], memory = null }) {
|
|
1147
961
|
return {
|
|
1148
962
|
...archives ? {
|
|
1149
963
|
archived: archives.records,
|
|
@@ -1161,16 +975,12 @@ function gcReportSections({ archives, skipped = [], deadProjects = [], orphanedP
|
|
|
1161
975
|
projectRoot,
|
|
1162
976
|
bytes: bytes ?? null
|
|
1163
977
|
})),
|
|
1164
|
-
linkedWorktrees: (worktreeSweep
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
willRemove: w.skipCode === null,
|
|
1171
|
-
reason: w.skipCode,
|
|
1172
|
-
detail: w.skipped ?? worktreeRemovalReason(w),
|
|
1173
|
-
eligibleAt: w.eligibleAt === null ? null : new Date(w.eligibleAt).toISOString()
|
|
978
|
+
linkedWorktrees: linkedWorktreesJson(worktreeSweep),
|
|
979
|
+
unlistedWorktreeDirectories: (worktreeSweep?.unlisted ?? []).map((dir) => ({
|
|
980
|
+
path: dir.path,
|
|
981
|
+
repository: dir.repository,
|
|
982
|
+
bytes: dir.bytes,
|
|
983
|
+
remedy: unlistedWorktreeRemedy(dir)
|
|
1174
984
|
})),
|
|
1175
985
|
parkedSimulators: parkedSims.map(({ udid, name, model, runtime, parkedAt, app, bytes, listed }) => ({
|
|
1176
986
|
udid,
|
|
@@ -1214,6 +1024,16 @@ function gcReportSections({ archives, skipped = [], deadProjects = [], orphanedP
|
|
|
1214
1024
|
idleDays: d.idleDays,
|
|
1215
1025
|
bytes: d.bytes ?? null
|
|
1216
1026
|
})),
|
|
1027
|
+
finishedDevices: finishedDevices.map((d) => ({
|
|
1028
|
+
kind: d.kind,
|
|
1029
|
+
id: d.id,
|
|
1030
|
+
name: d.name,
|
|
1031
|
+
project: d.project,
|
|
1032
|
+
slot: d.slot ?? null,
|
|
1033
|
+
bytes: d.bytes ?? null,
|
|
1034
|
+
reason: d.reason
|
|
1035
|
+
})),
|
|
1036
|
+
ownedDeviceDisk,
|
|
1217
1037
|
staleDeviceRecords: staleDeviceRecords.map((r) => ({
|
|
1218
1038
|
kind: r.kind,
|
|
1219
1039
|
id: r.id,
|
|
@@ -1325,12 +1145,203 @@ function gcReportSections({ archives, skipped = [], deadProjects = [], orphanedP
|
|
|
1325
1145
|
})),
|
|
1326
1146
|
agentDevice: agentDeviceUsage,
|
|
1327
1147
|
swiftpmCache: swiftpmCacheUsage,
|
|
1148
|
+
machineCaches,
|
|
1149
|
+
tempLeftovers: tempLeftoverSection(tempLeftovers),
|
|
1150
|
+
gradleVersions,
|
|
1328
1151
|
memory: memory?.processes ?? [],
|
|
1329
1152
|
watchmanRoots: memory?.watchmanRoots ?? [],
|
|
1330
1153
|
memoryNotices: (memory?.notices ?? []).map((message) => ({ message }))
|
|
1331
1154
|
};
|
|
1332
1155
|
}
|
|
1333
1156
|
//#endregion
|
|
1157
|
+
//#region src/commands/gc/worktree-sizes.ts
|
|
1158
|
+
const SIZE_TIMEOUT_MS = 3e4;
|
|
1159
|
+
const BUILD_STATE_MAX_AGE_MS = 36e5;
|
|
1160
|
+
const BUILD_STATE_TIMEOUT_MS = 1e4;
|
|
1161
|
+
const BUILD_STATE_CONCURRENCY = 4;
|
|
1162
|
+
const PNPM_STORE = ".pnpm-store";
|
|
1163
|
+
const WORKTREES_DIR = ".worktrees";
|
|
1164
|
+
function isDirectory(path) {
|
|
1165
|
+
try {
|
|
1166
|
+
return lstatSync(path).isDirectory();
|
|
1167
|
+
} catch {
|
|
1168
|
+
return false;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
function inside(root, path) {
|
|
1172
|
+
const rel = relative(root, path);
|
|
1173
|
+
return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel);
|
|
1174
|
+
}
|
|
1175
|
+
async function buildStateDirs(path, keys, operation) {
|
|
1176
|
+
const entries = await listWorktreesAsync(path, {
|
|
1177
|
+
env: operation?.env,
|
|
1178
|
+
signal: operation?.signal,
|
|
1179
|
+
declareFor: operation
|
|
1180
|
+
});
|
|
1181
|
+
const own = keys.filter((key) => matchWorktreeEntry(entries, key)?.path === path);
|
|
1182
|
+
const root = canonicalPath(path);
|
|
1183
|
+
const worktree = {
|
|
1184
|
+
path,
|
|
1185
|
+
linked: true,
|
|
1186
|
+
keys: own
|
|
1187
|
+
};
|
|
1188
|
+
const dirs = /* @__PURE__ */ new Map();
|
|
1189
|
+
const add = (dir) => {
|
|
1190
|
+
if (!isDirectory(dir)) return;
|
|
1191
|
+
const real = canonicalPath(dir);
|
|
1192
|
+
if (inside(root, real) && !dirs.has(real)) dirs.set(real, dir);
|
|
1193
|
+
};
|
|
1194
|
+
for (const kind of ["build", "dependencies"]) for (const base of appRoots(worktree, kind)) for (const dir of ARTIFACT_DIRS[kind]) add(join(base, dir));
|
|
1195
|
+
add(join(path, PNPM_STORE));
|
|
1196
|
+
return [...dirs.keys()];
|
|
1197
|
+
}
|
|
1198
|
+
function cachedSize(dir, now) {
|
|
1199
|
+
const cached = readDiskUsage(dir);
|
|
1200
|
+
if (!cached) return null;
|
|
1201
|
+
const at = Date.parse(cached.measuredAt);
|
|
1202
|
+
if (now - at >= BUILD_STATE_MAX_AGE_MS) return null;
|
|
1203
|
+
try {
|
|
1204
|
+
return statSync(dir).mtimeMs > at ? null : cached.bytes;
|
|
1205
|
+
} catch {
|
|
1206
|
+
return null;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
async function duSize(dir, operation) {
|
|
1210
|
+
let out;
|
|
1211
|
+
try {
|
|
1212
|
+
out = await runOperationFile(operation, getExecutor(), "du", ["-sk", dir], {
|
|
1213
|
+
timeoutMs: BUILD_STATE_TIMEOUT_MS,
|
|
1214
|
+
killSignal: "SIGKILL",
|
|
1215
|
+
cwd: operation?.cwd,
|
|
1216
|
+
signal: operation?.signal
|
|
1217
|
+
});
|
|
1218
|
+
} catch (error) {
|
|
1219
|
+
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
1220
|
+
operation?.signal.throwIfAborted();
|
|
1221
|
+
const stdout = error instanceof Error && "stdout" in error ? error.stdout : null;
|
|
1222
|
+
out = typeof stdout === "string" ? stdout : "";
|
|
1223
|
+
}
|
|
1224
|
+
const kb = /^(\d+)\t/.exec(out)?.[1];
|
|
1225
|
+
return kb ? Number(kb) * 1024 : null;
|
|
1226
|
+
}
|
|
1227
|
+
async function eachLimited(items, limit, run) {
|
|
1228
|
+
const queue = items[Symbol.iterator]();
|
|
1229
|
+
const worker = async () => {
|
|
1230
|
+
for (const item of queue) await run(item);
|
|
1231
|
+
};
|
|
1232
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Sizes the regenerable directories maintenance clears and a project-local `.pnpm-store` in each worktree of the
|
|
1236
|
+
* sweep, from the disk-usage cache while an entry is under an hour old and no entry was added to or removed from the
|
|
1237
|
+
* directory since. Stale directories are measured with one `du` each, sequentially with an operation and at most
|
|
1238
|
+
* 4 at once otherwise, with 10 seconds each, so a
|
|
1239
|
+
* slow tree delays only itself; a directory du could not measure in time is reported unmeasured (null). Also lists
|
|
1240
|
+
* the directories directly under a repository's `.worktrees/` that are no git worktree at all.
|
|
1241
|
+
*/
|
|
1242
|
+
async function measureWorktreeSweep(sweep, operation) {
|
|
1243
|
+
operation?.signal.throwIfAborted();
|
|
1244
|
+
const now = Date.now();
|
|
1245
|
+
const pending = [];
|
|
1246
|
+
for (const candidate of sweep.worktrees) {
|
|
1247
|
+
operation?.signal.throwIfAborted();
|
|
1248
|
+
if (candidate.skipCode === "not-a-worktree") continue;
|
|
1249
|
+
const dirs = await buildStateDirs(candidate.path, candidate.keys, operation);
|
|
1250
|
+
pending.push({
|
|
1251
|
+
candidate,
|
|
1252
|
+
root: canonicalPath(candidate.path),
|
|
1253
|
+
dirs,
|
|
1254
|
+
sizes: new Map(dirs.map((dir) => [dir, cachedSize(dir, now)]))
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
const due = pending.flatMap(({ sizes }) => [...sizes].flatMap(([dir, bytes]) => bytes === null ? [{
|
|
1258
|
+
dir,
|
|
1259
|
+
sizes
|
|
1260
|
+
}] : []));
|
|
1261
|
+
const worktrees = new Set(due.map(({ sizes }) => sizes)).size;
|
|
1262
|
+
if (due.length) phase("worktrees", `measuring the build state of ${plural(worktrees, "worktree")}`, operation);
|
|
1263
|
+
await eachLimited(due, operation ? 1 : BUILD_STATE_CONCURRENCY, async ({ dir, sizes }) => {
|
|
1264
|
+
operation?.signal.throwIfAborted();
|
|
1265
|
+
const measuredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1266
|
+
const bytes = await duSize(dir, operation);
|
|
1267
|
+
if (bytes === null) return;
|
|
1268
|
+
sizes.set(dir, bytes);
|
|
1269
|
+
operation?.signal.throwIfAborted();
|
|
1270
|
+
writeCacheFile(diskUsageCacheFile(dir), {
|
|
1271
|
+
path: dir,
|
|
1272
|
+
measuredAt,
|
|
1273
|
+
bytes
|
|
1274
|
+
});
|
|
1275
|
+
});
|
|
1276
|
+
for (const { candidate, root, dirs, sizes } of pending) {
|
|
1277
|
+
const entries = dirs.map((dir) => ({
|
|
1278
|
+
path: relative(root, dir).split(sep).join("/"),
|
|
1279
|
+
bytes: sizes.get(dir) ?? null
|
|
1280
|
+
}));
|
|
1281
|
+
candidate.buildState = {
|
|
1282
|
+
bytes: entries.every((dir) => dir.bytes !== null) ? entries.reduce((sum, dir) => sum + (dir.bytes ?? 0), 0) : null,
|
|
1283
|
+
dirs: entries.toSorted((a, b) => (b.bytes ?? 0) - (a.bytes ?? 0))
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
sweep.unlisted = await unlistedWorktreeDirs(operation);
|
|
1287
|
+
return sweep;
|
|
1288
|
+
}
|
|
1289
|
+
/** Whether `path` is a checkout git still knows: its own `.git` directory, or a `.git` file whose gitdir exists. */
|
|
1290
|
+
function hasLiveGitDir(path) {
|
|
1291
|
+
const dotGit = join(path, ".git");
|
|
1292
|
+
let text;
|
|
1293
|
+
try {
|
|
1294
|
+
if (lstatSync(dotGit).isDirectory()) return true;
|
|
1295
|
+
text = readFileSync(dotGit, "utf-8");
|
|
1296
|
+
} catch {
|
|
1297
|
+
return false;
|
|
1298
|
+
}
|
|
1299
|
+
const gitdir = /^gitdir:\s*(.+)$/m.exec(text)?.[1]?.trim();
|
|
1300
|
+
return Boolean(gitdir) && existsSync(resolve(dirname(dotGit), gitdir ?? ""));
|
|
1301
|
+
}
|
|
1302
|
+
async function unlistedWorktreeDirs(operation) {
|
|
1303
|
+
const repositories = /* @__PURE__ */ new Map();
|
|
1304
|
+
const options = {
|
|
1305
|
+
env: operation?.env,
|
|
1306
|
+
signal: operation?.signal,
|
|
1307
|
+
declareFor: operation
|
|
1308
|
+
};
|
|
1309
|
+
for (const root of await candidateRootsAsync(operation)) {
|
|
1310
|
+
operation?.signal.throwIfAborted();
|
|
1311
|
+
const { entries, ...source } = await resolveSourceCheckoutAsync(root, options);
|
|
1312
|
+
if ("refusal" in source || repositories.has(source.path)) continue;
|
|
1313
|
+
repositories.set(source.path, entries.map((entry) => canonicalPath(entry.path)));
|
|
1314
|
+
}
|
|
1315
|
+
const found = [];
|
|
1316
|
+
for (const [repository, listed] of repositories) {
|
|
1317
|
+
operation?.signal.throwIfAborted();
|
|
1318
|
+
const parent = join(repository, WORKTREES_DIR);
|
|
1319
|
+
let names;
|
|
1320
|
+
try {
|
|
1321
|
+
names = readdirSync(parent);
|
|
1322
|
+
} catch {
|
|
1323
|
+
continue;
|
|
1324
|
+
}
|
|
1325
|
+
for (const name of names.toSorted()) {
|
|
1326
|
+
operation?.signal.throwIfAborted();
|
|
1327
|
+
const path = join(parent, name);
|
|
1328
|
+
if (!isDirectory(path) || hasLiveGitDir(path)) continue;
|
|
1329
|
+
const real = canonicalPath(path);
|
|
1330
|
+
if (listed.some((entry) => entry === real || entry.startsWith(`${real}${sep}`))) continue;
|
|
1331
|
+
found.push({
|
|
1332
|
+
path,
|
|
1333
|
+
repository,
|
|
1334
|
+
bytes: await measuredDirectorySizeAsync(path, {
|
|
1335
|
+
timeoutMs: SIZE_TIMEOUT_MS,
|
|
1336
|
+
operation,
|
|
1337
|
+
signal: operation?.signal
|
|
1338
|
+
})
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
return found;
|
|
1343
|
+
}
|
|
1344
|
+
//#endregion
|
|
1334
1345
|
//#region src/commands/gc/ledger.ts
|
|
1335
1346
|
/** `sims` must be a complete listing, unavailable simulators included, taken after `ledger` was read. */
|
|
1336
1347
|
function findStaleLedgerEntries(ledger, sims) {
|
|
@@ -1361,15 +1372,23 @@ function isStaleAvdName(name, config) {
|
|
|
1361
1372
|
function reservedForSetup(name, config) {
|
|
1362
1373
|
return Object.values(config?.projects ?? {}).some((project) => projectDeviceSlots(project).some(({ platforms }) => platforms.android?.setupIncomplete && platforms.android.avdName === name));
|
|
1363
1374
|
}
|
|
1364
|
-
function forgetStaleAvdName(name) {
|
|
1375
|
+
async function forgetStaleAvdName(name, signal) {
|
|
1365
1376
|
let claim = null;
|
|
1366
1377
|
try {
|
|
1367
1378
|
claim = acquireAvdClaim(name);
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1379
|
+
while (true) try {
|
|
1380
|
+
return await forgetCreatedDeviceAsync("android", name, signal, (forget) => withDirLock(configLockPath(), () => {
|
|
1381
|
+
if (!isStaleAvdName(name, loadConfig$1())) return false;
|
|
1382
|
+
forget();
|
|
1383
|
+
return true;
|
|
1384
|
+
}, {
|
|
1385
|
+
waitMs: 0,
|
|
1386
|
+
ensureParent: () => ensurePrivateDir(getConfigDir())
|
|
1387
|
+
}));
|
|
1388
|
+
} catch (error) {
|
|
1389
|
+
if (!(error instanceof Error && "code" in error && error.code === "STIM_LOCK_TIMEOUT" && "lockPath" in error && error.lockPath === configLockPath())) throw error;
|
|
1390
|
+
await withConfigLockAsync$1(() => {}, signal);
|
|
1391
|
+
}
|
|
1373
1392
|
} finally {
|
|
1374
1393
|
(0, ownership_claim_exports.releaseClaim)(claim);
|
|
1375
1394
|
}
|
|
@@ -1396,23 +1415,28 @@ function isDanglingLink(path) {
|
|
|
1396
1415
|
return false;
|
|
1397
1416
|
}
|
|
1398
1417
|
}
|
|
1399
|
-
function forgetStaleLedgerEntries(entries) {
|
|
1418
|
+
async function forgetStaleLedgerEntries(entries, output = GC_OUTPUT, signal) {
|
|
1419
|
+
signal?.throwIfAborted();
|
|
1400
1420
|
let failures = 0;
|
|
1401
1421
|
for (const entry of entries) {
|
|
1422
|
+
await setImmediate();
|
|
1423
|
+
signal?.throwIfAborted();
|
|
1402
1424
|
if (entry.kind === "web" && !isStaleProfile(entry.id, existsSync)) continue;
|
|
1403
1425
|
try {
|
|
1404
1426
|
if (entry.kind === "android") {
|
|
1405
|
-
if (!forgetStaleAvdName(entry.id)) {
|
|
1406
|
-
|
|
1427
|
+
if (!await forgetStaleAvdName(entry.id, signal)) {
|
|
1428
|
+
output.log(chalk.dim(`Kept the ledger entry for android ${entry.id}: the AVD or its setup reappeared`));
|
|
1407
1429
|
continue;
|
|
1408
1430
|
}
|
|
1409
|
-
} else
|
|
1410
|
-
|
|
1431
|
+
} else await forgetCreatedDeviceAsync(entry.kind, entry.id, signal);
|
|
1432
|
+
output.log(chalk.green(`Forgot the ledger entry for ${entry.kind} ${entry.id} (not on this machine)`));
|
|
1411
1433
|
} catch (error) {
|
|
1434
|
+
signal?.throwIfAborted();
|
|
1412
1435
|
failures++;
|
|
1413
|
-
|
|
1436
|
+
output.log(chalk.red(`Could not forget the ledger entry for ${entry.kind} ${entry.id}: ${error?.message || error}`));
|
|
1414
1437
|
}
|
|
1415
1438
|
}
|
|
1439
|
+
signal?.throwIfAborted();
|
|
1416
1440
|
return failures;
|
|
1417
1441
|
}
|
|
1418
1442
|
//#endregion
|
|
@@ -1423,11 +1447,14 @@ const CAPPED_LOGS = [
|
|
|
1423
1447
|
"device.ndjson"
|
|
1424
1448
|
];
|
|
1425
1449
|
const TRIM_ABOVE_BYTES = 2 * LOG_ROTATE_BYTES;
|
|
1426
|
-
function fileSize(path) {
|
|
1450
|
+
async function fileSize(path, signal) {
|
|
1451
|
+
signal?.throwIfAborted();
|
|
1427
1452
|
try {
|
|
1428
|
-
const
|
|
1429
|
-
|
|
1453
|
+
const metadata = await stat(path);
|
|
1454
|
+
signal?.throwIfAborted();
|
|
1455
|
+
return metadata.isFile() ? metadata.size : 0;
|
|
1430
1456
|
} catch {
|
|
1457
|
+
signal?.throwIfAborted();
|
|
1431
1458
|
return 0;
|
|
1432
1459
|
}
|
|
1433
1460
|
}
|
|
@@ -1446,29 +1473,36 @@ function cappedFiles(logsDir) {
|
|
|
1446
1473
|
function trimmable(size) {
|
|
1447
1474
|
return size > TRIM_ABOVE_BYTES ? size - LOG_ROTATE_BYTES : 0;
|
|
1448
1475
|
}
|
|
1449
|
-
function excess(logsDir) {
|
|
1450
|
-
|
|
1476
|
+
async function excess(logsDir, signal) {
|
|
1477
|
+
let bytes = 0;
|
|
1478
|
+
for (const { file } of cappedFiles(logsDir)) bytes += trimmable(await fileSize(file, signal));
|
|
1479
|
+
return bytes;
|
|
1451
1480
|
}
|
|
1452
1481
|
function collectorReason(root) {
|
|
1453
1482
|
const platforms = Object.keys(readCollectors(root));
|
|
1454
1483
|
return platforms.length ? `a device log collector is recorded for ${platforms.join(", ")}` : null;
|
|
1455
1484
|
}
|
|
1456
|
-
function collectWorkspaceLogs({ exclude = [] } = {}) {
|
|
1485
|
+
async function collectWorkspaceLogs({ exclude = [], operation } = {}) {
|
|
1486
|
+
const signal = operation?.signal;
|
|
1487
|
+
signal?.throwIfAborted();
|
|
1457
1488
|
const mountedVolumes = listMountedVolumes();
|
|
1458
1489
|
const entries = [];
|
|
1459
1490
|
for (const { dir, projectRoot, problem } of listWorkspaceDirs()) {
|
|
1491
|
+
signal?.throwIfAborted();
|
|
1460
1492
|
if (exclude.includes(dir)) continue;
|
|
1461
1493
|
if (projectRoot !== null && !isOnMountedVolume(projectRoot, mountedVolumes)) continue;
|
|
1462
1494
|
const logsDir = join$1(dir, "logs");
|
|
1463
1495
|
let names;
|
|
1464
1496
|
try {
|
|
1465
|
-
names =
|
|
1497
|
+
names = await readdir(logsDir);
|
|
1466
1498
|
} catch {
|
|
1499
|
+
signal?.throwIfAborted();
|
|
1467
1500
|
continue;
|
|
1468
1501
|
}
|
|
1469
|
-
|
|
1502
|
+
let bytes = 0;
|
|
1503
|
+
for (const name of names) bytes += await fileSize(join$1(logsDir, name), signal);
|
|
1470
1504
|
if (bytes === 0) continue;
|
|
1471
|
-
const trimBytes = excess(logsDir);
|
|
1505
|
+
const trimBytes = await excess(logsDir, signal);
|
|
1472
1506
|
let keptCode = null;
|
|
1473
1507
|
let keptReason = null;
|
|
1474
1508
|
if (trimBytes > 0) {
|
|
@@ -1476,7 +1510,7 @@ function collectWorkspaceLogs({ exclude = [] } = {}) {
|
|
|
1476
1510
|
keptCode = "unresolved";
|
|
1477
1511
|
keptReason = `workspace directory not resolved: ${problem ?? "unknown project root"}`;
|
|
1478
1512
|
} else {
|
|
1479
|
-
const inUse =
|
|
1513
|
+
const inUse = await workspaceInUseAsync(projectRoot, {}, signal, operation);
|
|
1480
1514
|
const collector = collectorReason(projectRoot);
|
|
1481
1515
|
if (inUse.length) {
|
|
1482
1516
|
keptCode = "in-use";
|
|
@@ -1497,333 +1531,92 @@ function collectWorkspaceLogs({ exclude = [] } = {}) {
|
|
|
1497
1531
|
keptReason
|
|
1498
1532
|
});
|
|
1499
1533
|
}
|
|
1534
|
+
signal?.throwIfAborted();
|
|
1500
1535
|
return entries;
|
|
1501
1536
|
}
|
|
1502
|
-
function trimToNewest({ file, log }, maxBytes) {
|
|
1503
|
-
if (!trimmable(fileSize(file))) return 0;
|
|
1504
|
-
return
|
|
1505
|
-
|
|
1537
|
+
async function trimToNewest({ file, log }, maxBytes, signal) {
|
|
1538
|
+
if (!trimmable(await fileSize(file, signal))) return 0;
|
|
1539
|
+
return withDirLockTransaction(`${log}.lock`, async () => {
|
|
1540
|
+
signal?.throwIfAborted();
|
|
1541
|
+
const { size, atime, mtime } = await stat(file);
|
|
1542
|
+
signal?.throwIfAborted();
|
|
1506
1543
|
if (!trimmable(size)) return 0;
|
|
1507
1544
|
const tail = Buffer.alloc(maxBytes);
|
|
1508
|
-
const fd =
|
|
1545
|
+
const fd = await open(file, "r");
|
|
1509
1546
|
try {
|
|
1510
|
-
|
|
1547
|
+
signal?.throwIfAborted();
|
|
1548
|
+
await fd.read(tail, 0, maxBytes, size - maxBytes);
|
|
1511
1549
|
} finally {
|
|
1512
|
-
|
|
1550
|
+
await fd.close();
|
|
1513
1551
|
}
|
|
1552
|
+
signal?.throwIfAborted();
|
|
1514
1553
|
const newline = tail.indexOf(10);
|
|
1515
1554
|
const kept = newline === -1 ? Buffer.alloc(0) : tail.subarray(newline + 1);
|
|
1516
1555
|
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
1517
1556
|
try {
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1557
|
+
await writeFile(tmp, kept, { signal });
|
|
1558
|
+
await utimes(tmp, atime, mtime);
|
|
1559
|
+
signal?.throwIfAborted();
|
|
1560
|
+
await rename(tmp, file);
|
|
1561
|
+
} finally {
|
|
1562
|
+
await rm(tmp, { force: true });
|
|
1524
1563
|
}
|
|
1525
1564
|
return size - kept.length;
|
|
1526
|
-
});
|
|
1565
|
+
}, { signal });
|
|
1527
1566
|
}
|
|
1528
|
-
async function trimWorkspaceLogs(entries) {
|
|
1567
|
+
async function trimWorkspaceLogs(entries, output = GC_OUTPUT, operation) {
|
|
1568
|
+
const signal = operation?.signal;
|
|
1569
|
+
signal?.throwIfAborted();
|
|
1529
1570
|
let failures = 0;
|
|
1530
1571
|
let trimmed = 0;
|
|
1531
1572
|
for (const entry of entries) {
|
|
1573
|
+
signal?.throwIfAborted();
|
|
1532
1574
|
const root = entry.projectRoot;
|
|
1533
1575
|
if (!entry.willTrim || root === null) {
|
|
1534
|
-
if (entry.keptReason)
|
|
1576
|
+
if (entry.keptReason) output.log(chalk.dim(`Kept the logs of ${root ?? entry.dir}: ${entry.keptReason}`));
|
|
1535
1577
|
continue;
|
|
1536
1578
|
}
|
|
1537
1579
|
let run;
|
|
1538
1580
|
try {
|
|
1539
|
-
run = await withIdleWorkspace(root, () => {
|
|
1581
|
+
run = await withIdleWorkspace(root, async () => {
|
|
1540
1582
|
const collector = collectorReason(root);
|
|
1541
1583
|
if (collector) return {
|
|
1542
1584
|
kept: collector,
|
|
1543
1585
|
bytes: 0
|
|
1544
1586
|
};
|
|
1587
|
+
let bytes = 0;
|
|
1588
|
+
for (const capped of cappedFiles(join$1(entry.dir, "logs"))) {
|
|
1589
|
+
signal?.throwIfAborted();
|
|
1590
|
+
bytes += await trimToNewest(capped, LOG_ROTATE_BYTES, signal);
|
|
1591
|
+
}
|
|
1545
1592
|
return {
|
|
1546
1593
|
kept: null,
|
|
1547
|
-
bytes
|
|
1594
|
+
bytes
|
|
1548
1595
|
};
|
|
1549
|
-
}, {
|
|
1596
|
+
}, {
|
|
1597
|
+
purpose: "gc",
|
|
1598
|
+
operation
|
|
1599
|
+
});
|
|
1550
1600
|
} catch (error) {
|
|
1601
|
+
if (error instanceof WorkspaceProcessCleanupError) throw error;
|
|
1602
|
+
signal?.throwIfAborted();
|
|
1551
1603
|
failures++;
|
|
1552
|
-
|
|
1604
|
+
output.log(chalk.red(`Could not trim the logs of ${root}: ${error?.message || String(error)}`));
|
|
1553
1605
|
continue;
|
|
1554
1606
|
}
|
|
1555
1607
|
if (!run.ran || run.value.kept) {
|
|
1556
1608
|
const kept = run.ran ? run.value.kept : `in use: ${run.reasons.join("; ")}`;
|
|
1557
|
-
|
|
1609
|
+
output.log(chalk.yellow(`Kept the logs of ${root}: ${kept}`));
|
|
1558
1610
|
continue;
|
|
1559
1611
|
}
|
|
1560
1612
|
trimmed += run.value.bytes;
|
|
1561
|
-
|
|
1613
|
+
output.log(chalk.green(`Trimmed the logs of ${root} (${formatBytes(run.value.bytes)})`));
|
|
1562
1614
|
}
|
|
1563
|
-
|
|
1615
|
+
signal?.throwIfAborted();
|
|
1616
|
+
if (trimmed) output.log(chalk.dim(`Trimmed ${formatBytes(trimmed)} of workspace logs.`));
|
|
1564
1617
|
return failures;
|
|
1565
1618
|
}
|
|
1566
1619
|
//#endregion
|
|
1567
|
-
//#region src/commands/gc/inventory.ts
|
|
1568
|
-
const IOS_SIMULATOR_PLATFORM = "com.apple.platform.iphonesimulator";
|
|
1569
|
-
function text(value) {
|
|
1570
|
-
return typeof value === "string" && value.length > 0 ? value : null;
|
|
1571
|
-
}
|
|
1572
|
-
function parseSimRuntimeList(jsonOutput) {
|
|
1573
|
-
const parsed = JSON.parse(jsonOutput);
|
|
1574
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Expected simctl runtime list to return a JSON object.");
|
|
1575
|
-
const runtimes = [];
|
|
1576
|
-
for (const [key, value] of Object.entries(parsed)) {
|
|
1577
|
-
if (!isJsonObject(value)) continue;
|
|
1578
|
-
const entry = value;
|
|
1579
|
-
if (entry.platformIdentifier !== IOS_SIMULATOR_PLATFORM) continue;
|
|
1580
|
-
runtimes.push({
|
|
1581
|
-
identifier: text(entry.identifier) ?? key,
|
|
1582
|
-
runtimeIdentifier: text(entry.runtimeIdentifier),
|
|
1583
|
-
version: text(entry.version),
|
|
1584
|
-
build: text(entry.build),
|
|
1585
|
-
sizeBytes: typeof entry.sizeBytes === "number" && Number.isFinite(entry.sizeBytes) ? entry.sizeBytes : null,
|
|
1586
|
-
deletable: entry.deletable === true,
|
|
1587
|
-
lastUsedAt: text(entry.lastUsedAt)
|
|
1588
|
-
});
|
|
1589
|
-
}
|
|
1590
|
-
return runtimes;
|
|
1591
|
-
}
|
|
1592
|
-
function unlistedRuntimes(jsonOutput, images) {
|
|
1593
|
-
const known = new Set(images.map((image) => image.runtimeIdentifier));
|
|
1594
|
-
const parsed = JSON.parse(jsonOutput);
|
|
1595
|
-
if (!Array.isArray(parsed.runtimes)) throw new Error("Expected simctl list runtimes to return a runtimes array.");
|
|
1596
|
-
return parsed.runtimes.flatMap((value) => {
|
|
1597
|
-
if (value === null || typeof value !== "object") return [];
|
|
1598
|
-
const entry = value;
|
|
1599
|
-
const identifier = text(entry.identifier);
|
|
1600
|
-
if (!identifier || !/\.iOS-/.test(identifier) || known.has(identifier)) return [];
|
|
1601
|
-
return [{
|
|
1602
|
-
identifier,
|
|
1603
|
-
runtimeIdentifier: identifier,
|
|
1604
|
-
version: text(entry.version),
|
|
1605
|
-
build: text(entry.buildversion),
|
|
1606
|
-
sizeBytes: null,
|
|
1607
|
-
deletable: false,
|
|
1608
|
-
lastUsedAt: null
|
|
1609
|
-
}];
|
|
1610
|
-
});
|
|
1611
|
-
}
|
|
1612
|
-
function modelName(deviceTypeIdentifier) {
|
|
1613
|
-
return deviceTypeIdentifier.replace(/^com\.apple\.CoreSimulator\.SimDeviceType\./, "").replaceAll("-", " ");
|
|
1614
|
-
}
|
|
1615
|
-
function sdkmanagerCommand(pkg) {
|
|
1616
|
-
const tool = androidToolPath("sdkmanager");
|
|
1617
|
-
return `${tool === "sdkmanager" ? tool : JSON.stringify(tool)} --uninstall ${JSON.stringify(pkg)}`;
|
|
1618
|
-
}
|
|
1619
|
-
function buildInventory({ sims, avds, runtimes, systemImages, config, deadProjects = [] }) {
|
|
1620
|
-
const dead = new Set(deadProjects);
|
|
1621
|
-
const assigned = /* @__PURE__ */ new Map();
|
|
1622
|
-
for (const [project, record] of Object.entries(config?.projects ?? {})) {
|
|
1623
|
-
if (dead.has(project)) continue;
|
|
1624
|
-
let slots;
|
|
1625
|
-
try {
|
|
1626
|
-
slots = projectDeviceSlots(record);
|
|
1627
|
-
} catch {
|
|
1628
|
-
continue;
|
|
1629
|
-
}
|
|
1630
|
-
for (const { slot, platforms } of slots) {
|
|
1631
|
-
if (platforms.ios?.deviceUdid) assigned.set(`ios:${platforms.ios.deviceUdid}`, {
|
|
1632
|
-
project,
|
|
1633
|
-
slot
|
|
1634
|
-
});
|
|
1635
|
-
if (platforms.android?.avdName) assigned.set(`android:${platforms.android.avdName}`, {
|
|
1636
|
-
project,
|
|
1637
|
-
slot
|
|
1638
|
-
});
|
|
1639
|
-
}
|
|
1640
|
-
}
|
|
1641
|
-
const parked = /* @__PURE__ */ new Set([...readParked("ios", { config }).map((sim) => `ios:${sim.udid}`), ...readParked("android", { config }).map((avd) => `android:${avd.name}`)]);
|
|
1642
|
-
const owner = (key, name, created) => {
|
|
1643
|
-
const workspace = assigned.get(key);
|
|
1644
|
-
if (workspace && created()) return {
|
|
1645
|
-
owner: "workspace",
|
|
1646
|
-
project: workspace.project,
|
|
1647
|
-
slot: workspace.slot
|
|
1648
|
-
};
|
|
1649
|
-
if (parked.has(key)) return {
|
|
1650
|
-
owner: "parked",
|
|
1651
|
-
project: null,
|
|
1652
|
-
slot: null
|
|
1653
|
-
};
|
|
1654
|
-
if (!name.startsWith("stim-")) return {
|
|
1655
|
-
owner: "user",
|
|
1656
|
-
project: null,
|
|
1657
|
-
slot: null
|
|
1658
|
-
};
|
|
1659
|
-
return {
|
|
1660
|
-
owner: created() ? "orphaned" : "otherStimHome",
|
|
1661
|
-
project: null,
|
|
1662
|
-
slot: null
|
|
1663
|
-
};
|
|
1664
|
-
};
|
|
1665
|
-
const devices = [...sims.map((sim) => ({
|
|
1666
|
-
kind: "ios",
|
|
1667
|
-
id: sim.udid,
|
|
1668
|
-
name: sim.name,
|
|
1669
|
-
model: modelName(sim.deviceTypeIdentifier),
|
|
1670
|
-
runtime: sim.runtime,
|
|
1671
|
-
state: sim.state,
|
|
1672
|
-
lastUsedAt: sim.lastUsedAt ?? null,
|
|
1673
|
-
bytes: sim.dataPathSize ?? null,
|
|
1674
|
-
directory: sim.dataPath ? sim.dataPath.replace(/\/data\/?$/, "") : null,
|
|
1675
|
-
...owner(`ios:${sim.udid}`, sim.name, () => isStimOwnedSim({
|
|
1676
|
-
udid: sim.udid,
|
|
1677
|
-
name: sim.name
|
|
1678
|
-
}))
|
|
1679
|
-
})), ...avds.map((avd) => ({
|
|
1680
|
-
kind: "android",
|
|
1681
|
-
id: avd.name,
|
|
1682
|
-
name: avd.name,
|
|
1683
|
-
model: avd.deviceProfile,
|
|
1684
|
-
runtime: avd.systemImage,
|
|
1685
|
-
state: null,
|
|
1686
|
-
lastUsedAt: avd.lastUsedAt,
|
|
1687
|
-
bytes: null,
|
|
1688
|
-
directory: avd.directory,
|
|
1689
|
-
...owner(`android:${avd.name}`, avd.name, () => isStimOwnedAvd(avd.name))
|
|
1690
|
-
}))];
|
|
1691
|
-
const simsByRuntime = /* @__PURE__ */ new Map();
|
|
1692
|
-
for (const sim of sims) simsByRuntime.set(sim.runtime, (simsByRuntime.get(sim.runtime) ?? 0) + 1);
|
|
1693
|
-
const avdsByImage = /* @__PURE__ */ new Map();
|
|
1694
|
-
for (const avd of avds) if (avd.systemImage) avdsByImage.set(avd.systemImage, (avdsByImage.get(avd.systemImage) ?? 0) + 1);
|
|
1695
|
-
return {
|
|
1696
|
-
devices,
|
|
1697
|
-
runtimes: runtimes.map((runtime) => ({
|
|
1698
|
-
identifier: runtime.identifier,
|
|
1699
|
-
runtimeIdentifier: runtime.runtimeIdentifier,
|
|
1700
|
-
version: runtime.version,
|
|
1701
|
-
build: runtime.build,
|
|
1702
|
-
bytes: runtime.sizeBytes,
|
|
1703
|
-
lastUsedAt: runtime.lastUsedAt,
|
|
1704
|
-
deviceCount: runtime.runtimeIdentifier ? simsByRuntime.get(runtime.runtimeIdentifier) ?? 0 : 0,
|
|
1705
|
-
command: runtime.deletable ? `xcrun simctl runtime delete ${runtime.identifier}` : null
|
|
1706
|
-
})),
|
|
1707
|
-
systemImages: systemImages.map((image) => ({
|
|
1708
|
-
package: image.pkg,
|
|
1709
|
-
directory: image.directory,
|
|
1710
|
-
avdCount: avdsByImage.get(image.pkg) ?? 0,
|
|
1711
|
-
command: sdkmanagerCommand(image.pkg)
|
|
1712
|
-
}))
|
|
1713
|
-
};
|
|
1714
|
-
}
|
|
1715
|
-
function modifiedAt(path) {
|
|
1716
|
-
try {
|
|
1717
|
-
return statSync(path).mtime.toISOString();
|
|
1718
|
-
} catch {
|
|
1719
|
-
return null;
|
|
1720
|
-
}
|
|
1721
|
-
}
|
|
1722
|
-
function isMissing(error) {
|
|
1723
|
-
const code = error.code;
|
|
1724
|
-
return code === "ENOENT" || code === "ENOTDIR";
|
|
1725
|
-
}
|
|
1726
|
-
function unreadableNotice(what, error) {
|
|
1727
|
-
const notice = `${what} not listed: ${describeError(error)}`;
|
|
1728
|
-
if (process.platform !== "darwin" || error.code !== "EPERM") return notice;
|
|
1729
|
-
return `${notice}. macOS privacy protection blocked the read; allow ${process.env.STIM_DESKTOP_APP ? "Stim Desktop" : "the app that runs stim"} under System Settings > Privacy & Security > Files and Folders (Removable Volumes for an external disk) or Full Disk Access`;
|
|
1730
|
-
}
|
|
1731
|
-
function listAvdRecords(notices, roots = avdStorageRoots()) {
|
|
1732
|
-
const records = /* @__PURE__ */ new Map();
|
|
1733
|
-
for (const root of roots) {
|
|
1734
|
-
let names;
|
|
1735
|
-
try {
|
|
1736
|
-
names = readdirSync(root);
|
|
1737
|
-
} catch (error) {
|
|
1738
|
-
if (!isMissing(error)) notices.push(unreadableNotice(`AVDs in ${root}`, error));
|
|
1739
|
-
continue;
|
|
1740
|
-
}
|
|
1741
|
-
for (const entry of names) {
|
|
1742
|
-
if (!entry.endsWith(".ini")) continue;
|
|
1743
|
-
const name = entry.slice(0, -4);
|
|
1744
|
-
if (records.has(name)) continue;
|
|
1745
|
-
let ini;
|
|
1746
|
-
try {
|
|
1747
|
-
ini = readFileSync(join(root, entry), "utf8");
|
|
1748
|
-
} catch (error) {
|
|
1749
|
-
if (!isMissing(error)) notices.push(unreadableNotice(`AVD ${name}`, error));
|
|
1750
|
-
continue;
|
|
1751
|
-
}
|
|
1752
|
-
let directory = null;
|
|
1753
|
-
let config = "";
|
|
1754
|
-
let unreadable = null;
|
|
1755
|
-
for (const candidate of avdIniPaths(root, ini)) try {
|
|
1756
|
-
config = readFileSync(join(candidate, "config.ini"), "utf8");
|
|
1757
|
-
directory = candidate;
|
|
1758
|
-
break;
|
|
1759
|
-
} catch (error) {
|
|
1760
|
-
if (!isMissing(error)) unreadable ??= error;
|
|
1761
|
-
}
|
|
1762
|
-
if (!directory && unreadable) notices.push(unreadableNotice(`details of AVD ${name}`, unreadable));
|
|
1763
|
-
records.set(name, {
|
|
1764
|
-
name,
|
|
1765
|
-
directory,
|
|
1766
|
-
systemImage: parseAvdSystemImage(config),
|
|
1767
|
-
deviceProfile: parseAvdDeviceProfile(config),
|
|
1768
|
-
lastUsedAt: directory ? modifiedAt(join(directory, "hardware-qemu.ini")) : null
|
|
1769
|
-
});
|
|
1770
|
-
}
|
|
1771
|
-
}
|
|
1772
|
-
return [...records.values()];
|
|
1773
|
-
}
|
|
1774
|
-
function simctl(args) {
|
|
1775
|
-
return getExecutor().runFile("xcrun", ["simctl", ...args], {
|
|
1776
|
-
timeoutMs: DEVICE_LIST_TIMEOUT_MS,
|
|
1777
|
-
killSignal: "SIGKILL"
|
|
1778
|
-
});
|
|
1779
|
-
}
|
|
1780
|
-
function collectInventory(config, deadProjects) {
|
|
1781
|
-
const notices = [];
|
|
1782
|
-
let sims = [];
|
|
1783
|
-
let runtimes = [];
|
|
1784
|
-
if (process.platform === "darwin") {
|
|
1785
|
-
try {
|
|
1786
|
-
sims = listAllIosSims({ timeoutMs: DEVICE_LIST_TIMEOUT_MS });
|
|
1787
|
-
} catch (error) {
|
|
1788
|
-
notices.push(`simulators not listed: ${describeError(error)}`);
|
|
1789
|
-
}
|
|
1790
|
-
try {
|
|
1791
|
-
runtimes = parseSimRuntimeList(simctl([
|
|
1792
|
-
"runtime",
|
|
1793
|
-
"list",
|
|
1794
|
-
"-j"
|
|
1795
|
-
]));
|
|
1796
|
-
} catch (error) {
|
|
1797
|
-
notices.push(`simulator runtime images not listed: ${describeError(error)}`);
|
|
1798
|
-
}
|
|
1799
|
-
try {
|
|
1800
|
-
runtimes.push(...unlistedRuntimes(simctl([
|
|
1801
|
-
"list",
|
|
1802
|
-
"runtimes",
|
|
1803
|
-
"-j"
|
|
1804
|
-
]), runtimes));
|
|
1805
|
-
} catch (error) {
|
|
1806
|
-
notices.push(`simulator runtimes not listed: ${describeError(error)}`);
|
|
1807
|
-
}
|
|
1808
|
-
}
|
|
1809
|
-
const imagesRoot = join(androidHome(), "system-images");
|
|
1810
|
-
const images = listInstalledSystemImages((error) => notices.push(unreadableNotice("Android system images", error))).map((image) => ({
|
|
1811
|
-
pkg: image.pkg,
|
|
1812
|
-
directory: join(imagesRoot, `android-${image.api}`, image.tag, image.arch)
|
|
1813
|
-
}));
|
|
1814
|
-
return {
|
|
1815
|
-
...buildInventory({
|
|
1816
|
-
sims,
|
|
1817
|
-
avds: listAvdRecords(notices),
|
|
1818
|
-
runtimes,
|
|
1819
|
-
systemImages: images,
|
|
1820
|
-
config,
|
|
1821
|
-
deadProjects
|
|
1822
|
-
}),
|
|
1823
|
-
notices
|
|
1824
|
-
};
|
|
1825
|
-
}
|
|
1826
|
-
//#endregion
|
|
1827
1620
|
//#region src/commands/gc/hosted-devices.ts
|
|
1828
1621
|
function collectParkedHostedDevices({ olderThanDays = null, now = Date.now() } = {}) {
|
|
1829
1622
|
const notices = [];
|
|
@@ -1872,15 +1665,17 @@ function collectParkedHostedDevices({ olderThanDays = null, now = Date.now() } =
|
|
|
1872
1665
|
notices
|
|
1873
1666
|
};
|
|
1874
1667
|
}
|
|
1875
|
-
async function stopHostedDevice(record, claim) {
|
|
1668
|
+
async function stopHostedDevice(record, claim, operation) {
|
|
1669
|
+
operation?.signal.throwIfAborted();
|
|
1876
1670
|
const home = join(deviceHostArea(record.id), "home");
|
|
1877
1671
|
(0, ownership_claim_exports.markClaimChildPending)(claim);
|
|
1672
|
+
if (operation) declareSpawnsOn(operation, claim);
|
|
1878
1673
|
let child;
|
|
1879
1674
|
try {
|
|
1880
1675
|
child = getExecutor().spawn(process.execPath, [join(dirname(fileURLToPath(import.meta.url)), "device-host-worker.mjs")], {
|
|
1881
1676
|
cwd: home,
|
|
1882
1677
|
env: {
|
|
1883
|
-
...process.env,
|
|
1678
|
+
...operation?.env ?? process.env,
|
|
1884
1679
|
STIM_HOME: home
|
|
1885
1680
|
},
|
|
1886
1681
|
detached: true,
|
|
@@ -1897,6 +1692,8 @@ async function stopHostedDevice(record, claim) {
|
|
|
1897
1692
|
value: null,
|
|
1898
1693
|
notice: describeError(error)
|
|
1899
1694
|
};
|
|
1695
|
+
} finally {
|
|
1696
|
+
if (operation) stopDeclaringSpawnsOn(operation, claim);
|
|
1900
1697
|
}
|
|
1901
1698
|
let identity = null;
|
|
1902
1699
|
let closed = false;
|
|
@@ -1917,6 +1714,7 @@ async function stopHostedDevice(record, claim) {
|
|
|
1917
1714
|
clearTimeout(killTimer);
|
|
1918
1715
|
clearTimeout(finishTimer);
|
|
1919
1716
|
clearTimeout(groupTimer);
|
|
1717
|
+
operation?.signal.removeEventListener("abort", cancel);
|
|
1920
1718
|
const settled = closed && (!child.pid || !(0, ownership_claim_exports.processGroupAlive)(child.pid));
|
|
1921
1719
|
if (settled) (0, ownership_claim_exports.clearClaimChild)(claim);
|
|
1922
1720
|
let value = null;
|
|
@@ -1941,12 +1739,12 @@ async function stopHostedDevice(record, claim) {
|
|
|
1941
1739
|
if (child.pid && identity && ["same", "gone"].includes((0, process_identity_exports.inspectProcessIdentity)(identity))) try {
|
|
1942
1740
|
process.kill(-child.pid, name);
|
|
1943
1741
|
} catch {}
|
|
1944
|
-
else if (!identity) child.
|
|
1742
|
+
else if (!identity) child.stdin?.destroy();
|
|
1945
1743
|
};
|
|
1946
1744
|
const cancel = () => {
|
|
1947
1745
|
if (finished || cancelling) return;
|
|
1948
1746
|
cancelling = true;
|
|
1949
|
-
notice ??= "Hosted worker exceeded its deadline.";
|
|
1747
|
+
notice ??= operation?.signal.aborted ? "Hosted cleanup cancelled." : "Hosted worker exceeded its deadline.";
|
|
1950
1748
|
signal("SIGTERM");
|
|
1951
1749
|
killTimer = setTimeout(() => signal("SIGKILL"), 5e3);
|
|
1952
1750
|
finishTimer = setTimeout(finish, 1e4);
|
|
@@ -1960,6 +1758,7 @@ async function stopHostedDevice(record, claim) {
|
|
|
1960
1758
|
}
|
|
1961
1759
|
};
|
|
1962
1760
|
timer = setTimeout(cancel, 9e4);
|
|
1761
|
+
operation?.signal.addEventListener("abort", cancel, { once: true });
|
|
1963
1762
|
child.stdout?.on("data", (chunk) => {
|
|
1964
1763
|
if (Buffer.byteLength(output) + chunk.length > 16384) {
|
|
1965
1764
|
notice = "Hosted worker output exceeded its bound.";
|
|
@@ -1992,6 +1791,10 @@ async function stopHostedDevice(record, claim) {
|
|
|
1992
1791
|
};
|
|
1993
1792
|
try {
|
|
1994
1793
|
(0, ownership_claim_exports.setClaimChild)(claim, identity);
|
|
1794
|
+
if (operation?.signal.aborted) {
|
|
1795
|
+
cancel();
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1995
1798
|
child.stdin?.end(JSON.stringify({
|
|
1996
1799
|
mode: "stop",
|
|
1997
1800
|
platform: record.platform,
|
|
@@ -2008,14 +1811,15 @@ async function stopHostedDevice(record, claim) {
|
|
|
2008
1811
|
}
|
|
2009
1812
|
});
|
|
2010
1813
|
}
|
|
2011
|
-
async function deleteParkedHostedDevices(devices, { listAvds: list
|
|
1814
|
+
async function deleteParkedHostedDevices(devices, { listAvds: list, operation } = {}, output = GC_OUTPUT) {
|
|
2012
1815
|
let failures = 0;
|
|
2013
1816
|
for (const device of devices) {
|
|
1817
|
+
operation?.signal.throwIfAborted();
|
|
2014
1818
|
if (device.listed === false) continue;
|
|
2015
1819
|
const label = `${device.platform} ${device.name ?? device.session} (hosted session ${device.session})`;
|
|
2016
1820
|
const kept = (detail) => {
|
|
2017
|
-
|
|
2018
|
-
|
|
1821
|
+
output.log(`Kept ${label}: ${detail}`);
|
|
1822
|
+
output.record("parkedHostedDevice", "kept", label, {
|
|
2019
1823
|
id: device.id,
|
|
2020
1824
|
detail
|
|
2021
1825
|
});
|
|
@@ -2057,22 +1861,42 @@ async function deleteParkedHostedDevices(devices, { listAvds: list = listAvds }
|
|
|
2057
1861
|
kept(describeError(error));
|
|
2058
1862
|
continue;
|
|
2059
1863
|
}
|
|
2060
|
-
if (device.platform === "android"
|
|
2061
|
-
|
|
2062
|
-
|
|
1864
|
+
if (device.platform === "android") {
|
|
1865
|
+
let avds;
|
|
1866
|
+
if (operation) declareSpawnsOn(operation, claim);
|
|
1867
|
+
try {
|
|
1868
|
+
avds = await (list ? list() : listAvdsAsync({
|
|
1869
|
+
operation,
|
|
1870
|
+
signal: operation?.signal
|
|
1871
|
+
}));
|
|
1872
|
+
} finally {
|
|
1873
|
+
if (operation) stopDeclaringSpawnsOn(operation, claim);
|
|
1874
|
+
}
|
|
1875
|
+
if (!avds.includes(device.id)) {
|
|
1876
|
+
kept("AVD not visible from this shell's Android environment; run gc with the server's ANDROID_AVD_HOME/HOME");
|
|
1877
|
+
continue;
|
|
1878
|
+
}
|
|
2063
1879
|
}
|
|
1880
|
+
operation?.signal.throwIfAborted();
|
|
2064
1881
|
settled = false;
|
|
2065
|
-
const outcome = await stopHostedDevice(current, claim);
|
|
1882
|
+
const outcome = await stopHostedDevice(current, claim, operation);
|
|
2066
1883
|
settled = outcome.settled;
|
|
1884
|
+
if (!settled) throw new WorkspaceProcessCleanupError([claim], outcome.notice ?? "Hosted cleanup could not be verified.", "Verify the hosted worker and its descendants have stopped.");
|
|
1885
|
+
operation?.signal.throwIfAborted();
|
|
2067
1886
|
const result = isJsonObject(outcome.value) ? outcome.value : null;
|
|
2068
1887
|
const stopped = result && parseHostedPlatformDevice(result.device, device.platform);
|
|
2069
|
-
if (
|
|
2070
|
-
|
|
2071
|
-
|
|
1888
|
+
if (outcome.notice || result?.state !== "stopped" || !stopped || hostedDeviceId(stopped) !== device.id) throw new Error(outcome.notice ?? (typeof result?.notice === "string" ? result.notice : "Hosted deletion could not be verified."));
|
|
1889
|
+
output.log(`Deleted ${label}`);
|
|
1890
|
+
output.record("parkedHostedDevice", "done", label, { id: device.id });
|
|
2072
1891
|
} catch (error) {
|
|
1892
|
+
if (claim && (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error)) {
|
|
1893
|
+
settled = false;
|
|
1894
|
+
throw Object.assign(new WorkspaceProcessCleanupError([.../* @__PURE__ */ new Set([claim, ...error instanceof WorkspaceProcessCleanupError ? error.claims : []])], error.message, "Verify the hosted device operation and its descendants have stopped."), { reportCleanup: true });
|
|
1895
|
+
}
|
|
1896
|
+
operation?.signal.throwIfAborted();
|
|
2073
1897
|
const detail = describeError(error);
|
|
2074
|
-
|
|
2075
|
-
|
|
1898
|
+
output.error(`Failed to delete ${label}: ${detail}`);
|
|
1899
|
+
output.record("parkedHostedDevice", "failed", label, {
|
|
2076
1900
|
id: device.id,
|
|
2077
1901
|
detail
|
|
2078
1902
|
});
|
|
@@ -2085,15 +1909,27 @@ async function deleteParkedHostedDevices(devices, { listAvds: list = listAvds }
|
|
|
2085
1909
|
}
|
|
2086
1910
|
//#endregion
|
|
2087
1911
|
//#region src/commands/gc.ts
|
|
2088
|
-
function
|
|
1912
|
+
function readGcReport(options = {}, deps = {}) {
|
|
1913
|
+
return sweep({
|
|
1914
|
+
olderThan: options.olderThan,
|
|
1915
|
+
cache: options.cache,
|
|
1916
|
+
worktrees: options.worktrees,
|
|
1917
|
+
json: true
|
|
1918
|
+
}, deps, {
|
|
1919
|
+
output: GC_OUTPUT,
|
|
1920
|
+
takeResults: () => []
|
|
1921
|
+
}, true);
|
|
1922
|
+
}
|
|
1923
|
+
function removeInvalidProjectEntries(invalidProjects, output, signal) {
|
|
2089
1924
|
for (const path of invalidProjects) {
|
|
1925
|
+
signal?.throwIfAborted();
|
|
2090
1926
|
removeProject(path);
|
|
2091
|
-
|
|
2092
|
-
|
|
1927
|
+
output.log(chalk.green(`Removed the invalid registry entry ${path}`));
|
|
1928
|
+
output.record("project", "done", path);
|
|
2093
1929
|
}
|
|
2094
1930
|
}
|
|
2095
|
-
function collectGcMemory(scope) {
|
|
2096
|
-
return memorySweepIsScoped() ? Promise.resolve(scopedMemoryReport()) : collectMemoryReport(scope, true);
|
|
1931
|
+
function collectGcMemory(scope, env, operation) {
|
|
1932
|
+
return memorySweepIsScoped(env) ? Promise.resolve(scopedMemoryReport()) : collectMemoryReport(scope, true, operation);
|
|
2097
1933
|
}
|
|
2098
1934
|
function classifyProjectEntries(cfg, mountedVolumes) {
|
|
2099
1935
|
const deadProjects = [];
|
|
@@ -2125,23 +1961,28 @@ function classifyProjectEntries(cfg, mountedVolumes) {
|
|
|
2125
1961
|
};
|
|
2126
1962
|
}
|
|
2127
1963
|
/** The workspace directories an unscoped run removes whole: those of dead projects and orphaned directories. */
|
|
2128
|
-
function removedWholeWorkspaceDirs() {
|
|
1964
|
+
async function removedWholeWorkspaceDirs(operation) {
|
|
2129
1965
|
const mountedVolumes = listMountedVolumes();
|
|
2130
1966
|
const cfg = loadConfig$1();
|
|
2131
1967
|
const { deadProjects } = classifyProjectEntries(cfg, mountedVolumes);
|
|
2132
|
-
const { orphaned } = collectOrphanedWorkspaces(Object.keys(cfg?.projects ?? {}), mountedVolumes, {
|
|
1968
|
+
const { orphaned } = await collectOrphanedWorkspaces(Object.keys(cfg?.projects ?? {}), mountedVolumes, {
|
|
1969
|
+
measure: false,
|
|
1970
|
+
operation
|
|
1971
|
+
});
|
|
2133
1972
|
return [...orphaned.map((entry) => entry.dir), ...deadProjects.map(workspaceDir)];
|
|
2134
1973
|
}
|
|
2135
|
-
async function collectGcReport({ olderThan = null, cache = null, worktrees = false, now = Date.now(), lastTouched = workspaceLastUsed } = {}, deps = {}) {
|
|
1974
|
+
async function collectGcReport({ olderThan = null, cache = null, worktrees = false, refresh = true, now = Date.now(), lastTouched = workspaceLastUsed } = {}, deps = {}) {
|
|
1975
|
+
const operation = deps.operation;
|
|
1976
|
+
const env = operation?.env ?? process.env;
|
|
2136
1977
|
const scope = typeof cache === "string" && cache.trim() ? cache : null;
|
|
2137
1978
|
const all = scope !== null && olderThan === null;
|
|
2138
1979
|
const withWorkspaces = includesWorkspaceOutputs(scope);
|
|
2139
|
-
const selected = selectCaches(discoverCaches(), scope).filter((c) => !withWorkspaces || !isInsideWorkspaces(c.dir));
|
|
2140
|
-
if (selected.length) phase("caches", `measuring ${plural(selected.length, "shared cache")}
|
|
2141
|
-
const sized = sizeCaches(selected);
|
|
2142
|
-
const caches = scope === null ? previewScopedEmptying(sized) : planCacheEmptying(sized, all);
|
|
1980
|
+
const selected = selectCaches(discoverCaches(env, operation?.cwd), scope).filter((c) => !withWorkspaces || !isInsideWorkspaces(c.dir));
|
|
1981
|
+
if (selected.length) phase("caches", `measuring ${plural(selected.length, "shared cache")}`, operation);
|
|
1982
|
+
const sized = await sizeCaches(selected, operation);
|
|
1983
|
+
const caches = scope === null ? previewScopedEmptying(sized, env) : planCacheEmptying(sized, all, env);
|
|
2143
1984
|
if (scope) return {
|
|
2144
|
-
archives: includesArchives(scope) ? collectArchives(scope, olderThan, now) : void 0,
|
|
1985
|
+
archives: includesArchives(scope) ? await collectArchives(scope, olderThan, now, operation?.signal) : void 0,
|
|
2145
1986
|
skipped: [],
|
|
2146
1987
|
deadProjects: [],
|
|
2147
1988
|
invalidProjects: [],
|
|
@@ -2149,6 +1990,8 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
|
|
|
2149
1990
|
orphanedDevices: [],
|
|
2150
1991
|
unverifiedDevices: [],
|
|
2151
1992
|
staleDevices: [],
|
|
1993
|
+
finishedDevices: [],
|
|
1994
|
+
ownedDeviceDisk: [],
|
|
2152
1995
|
staleDeviceRecords: [],
|
|
2153
1996
|
staleLedgerEntries: [],
|
|
2154
1997
|
staleEasLedgerEntries: [],
|
|
@@ -2175,45 +2018,65 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
|
|
|
2175
2018
|
notices: [],
|
|
2176
2019
|
deletionSafe: true
|
|
2177
2020
|
},
|
|
2178
|
-
parkedSims: includesParkedDevices(scope) ? collectParkedSims(deps, {
|
|
2021
|
+
parkedSims: includesParkedDevices(scope) ? await collectParkedSims(deps, {
|
|
2179
2022
|
olderThanDays: olderThan,
|
|
2180
2023
|
now
|
|
2181
2024
|
}) : [],
|
|
2182
|
-
parkedAvds: includesParkedDevices(scope) ? collectParkedAvds(deps, {
|
|
2025
|
+
parkedAvds: includesParkedDevices(scope) ? await collectParkedAvds(deps, {
|
|
2183
2026
|
olderThanDays: olderThan,
|
|
2184
2027
|
now
|
|
2185
2028
|
}) : [],
|
|
2186
2029
|
parkedHostedDevices: [],
|
|
2187
2030
|
caches,
|
|
2188
|
-
workspaceOutputs: withWorkspaces ? collectWorkspaceOutputs({
|
|
2031
|
+
workspaceOutputs: withWorkspaces ? await collectWorkspaceOutputs({
|
|
2189
2032
|
olderThan,
|
|
2190
2033
|
now,
|
|
2191
|
-
exclude: isWorkspacesScope(scope) ? removedWholeWorkspaceDirs() : []
|
|
2034
|
+
exclude: isWorkspacesScope(scope) ? await removedWholeWorkspaceDirs(operation) : [],
|
|
2035
|
+
signal: operation?.signal,
|
|
2036
|
+
operation
|
|
2192
2037
|
}) : null,
|
|
2193
2038
|
workspaceLogs: [],
|
|
2194
|
-
recordings: includesRecordings(scope) ? collectRecordings({
|
|
2039
|
+
recordings: includesRecordings(scope) ? await collectRecordings({
|
|
2195
2040
|
whole: all,
|
|
2196
2041
|
olderThan,
|
|
2197
2042
|
now
|
|
2198
|
-
}) : [],
|
|
2043
|
+
}, [], operation?.signal) : [],
|
|
2199
2044
|
worktreeSweep: null,
|
|
2200
|
-
memory: memoryCacheKind(scope) ? await collectGcMemory(memoryScope(scope)) : null,
|
|
2045
|
+
memory: memoryCacheKind(scope) ? await collectGcMemory(memoryScope(scope), env, operation) : null,
|
|
2201
2046
|
agentDeviceUsage: null,
|
|
2202
2047
|
swiftpmCacheUsage: null,
|
|
2048
|
+
machineCaches: null,
|
|
2049
|
+
tempLeftovers: null,
|
|
2050
|
+
gradleVersions: includesGradleVersions(scope) ? await collectGradleVersions({
|
|
2051
|
+
olderThanDays: olderThan,
|
|
2052
|
+
now,
|
|
2053
|
+
operation
|
|
2054
|
+
}) : null,
|
|
2203
2055
|
cacheScope: scope,
|
|
2204
2056
|
olderThan,
|
|
2205
2057
|
all
|
|
2206
2058
|
};
|
|
2207
|
-
phase("agent-device", "measuring report-only state");
|
|
2208
|
-
const agentDeviceUsage = await getAgentDeviceUsage();
|
|
2209
|
-
phase("SwiftPM cache", "measuring report-only state");
|
|
2210
|
-
const swiftpmCacheUsage = await getSwiftpmCacheUsage();
|
|
2059
|
+
phase("agent-device", "measuring report-only state", operation);
|
|
2060
|
+
const agentDeviceUsage = await getAgentDeviceUsage({ operation });
|
|
2061
|
+
phase("SwiftPM cache", "measuring report-only state", operation);
|
|
2062
|
+
const swiftpmCacheUsage = await getSwiftpmCacheUsage({ operation });
|
|
2063
|
+
phase("machine caches", "measuring report-only state", operation);
|
|
2064
|
+
const machineCaches = await getMachineCacheReport(agentDeviceUsage, { operation });
|
|
2065
|
+
if (!deviceSweepIsScoped(env)) phase("TMPDIR", "checking recognized leftovers", operation);
|
|
2066
|
+
const tempLeftovers = await collectTempLeftovers({
|
|
2067
|
+
now,
|
|
2068
|
+
env,
|
|
2069
|
+
signal: operation?.signal,
|
|
2070
|
+
operation
|
|
2071
|
+
});
|
|
2211
2072
|
const mountedVolumes = listMountedVolumes();
|
|
2212
2073
|
const cfg = loadConfig$1();
|
|
2213
2074
|
let easSessionSweep = deps.precollectedEasSessionSweep;
|
|
2214
2075
|
if (!easSessionSweep) try {
|
|
2215
|
-
easSessionSweep = collectEasSessionSweep(cfg, deps);
|
|
2076
|
+
easSessionSweep = await collectEasSessionSweep(cfg, deps);
|
|
2216
2077
|
} catch (error) {
|
|
2078
|
+
if (error instanceof WorkspaceProcessCleanupError) throw error;
|
|
2079
|
+
operation?.signal.throwIfAborted();
|
|
2217
2080
|
easSessionSweep = {
|
|
2218
2081
|
projectScope: null,
|
|
2219
2082
|
orphaned: [],
|
|
@@ -2221,16 +2084,16 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
|
|
|
2221
2084
|
deletionSafe: false
|
|
2222
2085
|
};
|
|
2223
2086
|
}
|
|
2224
|
-
const parkedSims = collectParkedSims(deps, {
|
|
2087
|
+
const parkedSims = await collectParkedSims(deps, {
|
|
2225
2088
|
olderThanDays: olderThan,
|
|
2226
2089
|
now
|
|
2227
2090
|
});
|
|
2228
|
-
const parkedAvds = collectParkedAvds(deps, {
|
|
2091
|
+
const parkedAvds = await collectParkedAvds(deps, {
|
|
2229
2092
|
olderThanDays: olderThan,
|
|
2230
2093
|
now
|
|
2231
2094
|
});
|
|
2232
2095
|
const { deadProjects, invalidProjects, skipped } = classifyProjectEntries(cfg, mountedVolumes);
|
|
2233
|
-
const workspaceDirs = collectOrphanedWorkspaces(Object.keys(cfg?.projects ?? {}), mountedVolumes);
|
|
2096
|
+
const workspaceDirs = await collectOrphanedWorkspaces(Object.keys(cfg?.projects ?? {}), mountedVolumes, { operation });
|
|
2234
2097
|
skipped.push(...workspaceDirs.skipped);
|
|
2235
2098
|
const statusCaches = collectStaleStatusCaches(mountedVolumes);
|
|
2236
2099
|
skipped.push(...statusCaches.skipped);
|
|
@@ -2245,38 +2108,74 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
|
|
|
2245
2108
|
let staleDeviceRecords = [];
|
|
2246
2109
|
let staleLedgerEntries = staleBrowserProfiles(readCreatedDevices$1());
|
|
2247
2110
|
let idleDevices = [];
|
|
2248
|
-
|
|
2249
|
-
|
|
2111
|
+
let listed = null;
|
|
2112
|
+
let deviceDisk = [];
|
|
2113
|
+
const avdSizes = /* @__PURE__ */ new Map();
|
|
2114
|
+
const avdSize = (dir, options) => {
|
|
2115
|
+
let bytes = avdSizes.get(dir);
|
|
2116
|
+
if (bytes === void 0) avdSizes.set(dir, bytes = Promise.resolve((deps.directorySize ?? directorySizeAsync)(dir, {
|
|
2117
|
+
...options,
|
|
2118
|
+
operation,
|
|
2119
|
+
signal: operation?.signal
|
|
2120
|
+
})));
|
|
2121
|
+
return bytes;
|
|
2122
|
+
};
|
|
2123
|
+
const unsweepableReason = cfg === null ? "no Stim config found" : deviceSweepIsScoped(env) ? "STIM_HOME scopes this config, but simulators and AVDs are machine-global" : null;
|
|
2124
|
+
phase("devices", "listing simulators and emulators", operation);
|
|
2250
2125
|
if (unsweepableReason) {
|
|
2251
2126
|
let simNames = [];
|
|
2252
2127
|
let avdNames = [];
|
|
2253
2128
|
try {
|
|
2254
|
-
simNames =
|
|
2255
|
-
|
|
2129
|
+
simNames = (await listAllIosSimsAsync({
|
|
2130
|
+
timeoutMs: DEVICE_LIST_TIMEOUT_MS,
|
|
2131
|
+
operation,
|
|
2132
|
+
signal: operation?.signal
|
|
2133
|
+
})).map((s) => s.name);
|
|
2134
|
+
} catch (error) {
|
|
2135
|
+
if (error instanceof WorkspaceProcessCleanupError) throw error;
|
|
2136
|
+
operation?.signal.throwIfAborted();
|
|
2137
|
+
}
|
|
2256
2138
|
try {
|
|
2257
|
-
avdNames =
|
|
2258
|
-
|
|
2139
|
+
avdNames = await listAvdsAsync({
|
|
2140
|
+
timeoutMs: DEVICE_LIST_TIMEOUT_MS,
|
|
2141
|
+
operation,
|
|
2142
|
+
signal: operation?.signal
|
|
2143
|
+
});
|
|
2144
|
+
} catch (error) {
|
|
2145
|
+
if (error instanceof WorkspaceProcessCleanupError) throw error;
|
|
2146
|
+
operation?.signal.throwIfAborted();
|
|
2147
|
+
}
|
|
2259
2148
|
deviceSweepNotices.push(...describeUnverifiableDevices(simNames, avdNames, { reason: unsweepableReason }));
|
|
2260
2149
|
} else {
|
|
2261
2150
|
let sims = [];
|
|
2262
2151
|
let simsChecked = true;
|
|
2263
2152
|
const ledger = readCreatedDevices$1();
|
|
2264
2153
|
try {
|
|
2265
|
-
const allSims =
|
|
2154
|
+
const allSims = await listAllIosSimsAsync({
|
|
2266
2155
|
timeoutMs: DEVICE_LIST_TIMEOUT_MS,
|
|
2267
|
-
includeUnavailable: true
|
|
2156
|
+
includeUnavailable: true,
|
|
2157
|
+
operation,
|
|
2158
|
+
signal: operation?.signal
|
|
2268
2159
|
});
|
|
2269
2160
|
staleLedgerEntries = [...staleLedgerEntries, ...findStaleLedgerEntries(ledger, allSims)];
|
|
2270
2161
|
sims = allSims.filter((sim) => sim.available);
|
|
2271
|
-
} catch {
|
|
2162
|
+
} catch (error) {
|
|
2163
|
+
if (error instanceof WorkspaceProcessCleanupError) throw error;
|
|
2164
|
+
operation?.signal.throwIfAborted();
|
|
2272
2165
|
simsChecked = false;
|
|
2273
2166
|
deviceSweepNotices.push(`ios device sweep skipped: simulator tooling did not answer within ${DEVICE_LIST_TIMEOUT_MS / 1e3}s`);
|
|
2274
2167
|
}
|
|
2275
2168
|
let avds = [];
|
|
2276
2169
|
let avdsChecked = true;
|
|
2277
2170
|
try {
|
|
2278
|
-
avds =
|
|
2279
|
-
|
|
2171
|
+
avds = await listAvdsAsync({
|
|
2172
|
+
timeoutMs: DEVICE_LIST_TIMEOUT_MS,
|
|
2173
|
+
operation,
|
|
2174
|
+
signal: operation?.signal
|
|
2175
|
+
});
|
|
2176
|
+
} catch (error) {
|
|
2177
|
+
if (error instanceof WorkspaceProcessCleanupError) throw error;
|
|
2178
|
+
operation?.signal.throwIfAborted();
|
|
2280
2179
|
avdsChecked = false;
|
|
2281
2180
|
deviceSweepNotices.push(`android device sweep skipped: emulator tooling did not answer within ${DEVICE_LIST_TIMEOUT_MS / 1e3}s`);
|
|
2282
2181
|
}
|
|
@@ -2302,14 +2201,31 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
|
|
|
2302
2201
|
const directories = device.kind === "android" && !registeredAvds.has(device.id) ? orphanedAvdDirectories.filter((entry) => entry.name === device.id) : [];
|
|
2303
2202
|
return directories.length ? directories.map(({ directory }) => Object.assign({}, device, { directory })) : [device];
|
|
2304
2203
|
});
|
|
2305
|
-
orphanedDevices = withAndroidAvdSizes(found.orphaned.flatMap((device) => {
|
|
2204
|
+
orphanedDevices = await withAndroidAvdSizes(found.orphaned.flatMap((device) => {
|
|
2306
2205
|
const directories = device.kind === "android" ? orphanedAvdDirectories.filter((entry) => entry.name === device.name) : [];
|
|
2307
2206
|
return directories.length ? directories.map((orphanedDirectory) => Object.assign({}, device, { orphanedDirectory })) : [device];
|
|
2308
2207
|
}), {
|
|
2309
2208
|
avdDirectory: deps.avdDirectory ?? ownedAvdDirectory,
|
|
2310
|
-
size:
|
|
2209
|
+
size: avdSize,
|
|
2210
|
+
operation
|
|
2311
2211
|
});
|
|
2312
|
-
idleDevices = collectIdleDevices(cfg, sims, deadProjects, now);
|
|
2212
|
+
idleDevices = operation ? await collectIdleDevicesAsync(cfg, sims, deadProjects, now, env, operation.signal, operation) : collectIdleDevices(cfg, sims, deadProjects, now);
|
|
2213
|
+
if (simsChecked || avdsChecked) {
|
|
2214
|
+
const lists = {
|
|
2215
|
+
sims,
|
|
2216
|
+
avds: [...registeredAvds]
|
|
2217
|
+
};
|
|
2218
|
+
listed = lists;
|
|
2219
|
+
let unmeasured = 0;
|
|
2220
|
+
deviceDisk = ownedDeviceDisk(await ownedDeviceUsage((_target, workspace) => {
|
|
2221
|
+
if (workspace) unmeasured++;
|
|
2222
|
+
}, {
|
|
2223
|
+
sims: () => lists.sims,
|
|
2224
|
+
avds: () => lists.avds,
|
|
2225
|
+
avdDirectory: deps.avdDirectory ?? ownedAvdDirectory,
|
|
2226
|
+
size: async (dir) => await avdSize(dir, { timeoutMs: 5e3 }) || null
|
|
2227
|
+
}), { android: unmeasured });
|
|
2228
|
+
}
|
|
2313
2229
|
staleDeviceRecords = findStaleDeviceRecords({
|
|
2314
2230
|
config: cfg,
|
|
2315
2231
|
sims,
|
|
@@ -2318,7 +2234,7 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
|
|
|
2318
2234
|
simsChecked,
|
|
2319
2235
|
avdsChecked
|
|
2320
2236
|
});
|
|
2321
|
-
if (olderThan !== null) staleDevices = withAndroidAvdSizes(findStaleProjectDevices({
|
|
2237
|
+
if (olderThan !== null) staleDevices = await withAndroidAvdSizes(findStaleProjectDevices({
|
|
2322
2238
|
config: cfg,
|
|
2323
2239
|
sims,
|
|
2324
2240
|
avds,
|
|
@@ -2328,13 +2244,21 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
|
|
|
2328
2244
|
deadProjects
|
|
2329
2245
|
}), {
|
|
2330
2246
|
avdDirectory: deps.avdDirectory ?? ownedAvdDirectory,
|
|
2331
|
-
size:
|
|
2247
|
+
size: avdSize,
|
|
2248
|
+
operation
|
|
2332
2249
|
});
|
|
2333
2250
|
}
|
|
2334
2251
|
const locks = listBuildLocks();
|
|
2335
2252
|
const slots = listBuildSlots();
|
|
2336
2253
|
const goneWorkspaceDirs = [...workspaceDirs.orphaned.map((entry) => entry.dir), ...deadProjects.map(workspaceDir)];
|
|
2337
2254
|
const deviceLeases = collectDeviceLeases(now);
|
|
2255
|
+
const worktreeSweep = await measureWorktreeSweep(await collectWorktreeSweep({
|
|
2256
|
+
idle: worktrees,
|
|
2257
|
+
olderThan,
|
|
2258
|
+
now,
|
|
2259
|
+
refresh,
|
|
2260
|
+
operation
|
|
2261
|
+
}), operation);
|
|
2338
2262
|
return {
|
|
2339
2263
|
skipped,
|
|
2340
2264
|
deadProjects,
|
|
@@ -2370,58 +2294,79 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
|
|
|
2370
2294
|
deviceSweepNotices,
|
|
2371
2295
|
easSessionSweep,
|
|
2372
2296
|
caches,
|
|
2373
|
-
workspaceOutputs: collectWorkspaceOutputs({
|
|
2297
|
+
workspaceOutputs: await collectWorkspaceOutputs({
|
|
2374
2298
|
olderThan,
|
|
2375
2299
|
now,
|
|
2376
|
-
exclude: goneWorkspaceDirs
|
|
2300
|
+
exclude: goneWorkspaceDirs,
|
|
2301
|
+
signal: operation?.signal,
|
|
2302
|
+
operation
|
|
2303
|
+
}),
|
|
2304
|
+
workspaceLogs: await collectWorkspaceLogs({
|
|
2305
|
+
exclude: goneWorkspaceDirs,
|
|
2306
|
+
operation
|
|
2377
2307
|
}),
|
|
2378
|
-
|
|
2379
|
-
recordings: collectRecordings({
|
|
2308
|
+
recordings: await collectRecordings({
|
|
2380
2309
|
whole: false,
|
|
2381
2310
|
olderThan,
|
|
2382
2311
|
now
|
|
2383
|
-
}, goneWorkspaceDirs),
|
|
2384
|
-
worktreeSweep
|
|
2385
|
-
|
|
2386
|
-
|
|
2312
|
+
}, goneWorkspaceDirs, operation?.signal),
|
|
2313
|
+
worktreeSweep,
|
|
2314
|
+
finishedDevices: listed ? collectFinishedDevices({
|
|
2315
|
+
config: cfg,
|
|
2316
|
+
...listed,
|
|
2317
|
+
sweep: worktreeSweep,
|
|
2387
2318
|
now
|
|
2388
|
-
}),
|
|
2319
|
+
}) : [],
|
|
2320
|
+
ownedDeviceDisk: deviceDisk,
|
|
2389
2321
|
memory: await collectGcMemory({
|
|
2390
2322
|
watchman: true,
|
|
2391
2323
|
gradle: true
|
|
2392
|
-
}),
|
|
2324
|
+
}, env, operation),
|
|
2393
2325
|
agentDeviceUsage,
|
|
2394
2326
|
swiftpmCacheUsage,
|
|
2327
|
+
machineCaches,
|
|
2328
|
+
tempLeftovers,
|
|
2395
2329
|
cacheScope: null,
|
|
2396
2330
|
olderThan,
|
|
2397
2331
|
all
|
|
2398
2332
|
};
|
|
2399
2333
|
}
|
|
2400
2334
|
async function runGc(opts = {}, deps = {}) {
|
|
2401
|
-
|
|
2402
|
-
await maintenanceGuardedSweep(opts, deps);
|
|
2403
|
-
return;
|
|
2404
|
-
}
|
|
2405
|
-
const log = console.log;
|
|
2406
|
-
console.log = console.error;
|
|
2407
|
-
let payload;
|
|
2335
|
+
let result;
|
|
2408
2336
|
try {
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2337
|
+
result = await withGcMaintenanceClaim(opts, (claim) => executeGc(opts, deps, (stream, line) => {
|
|
2338
|
+
if (stream === "stderr" || opts.json) console.error(line);
|
|
2339
|
+
else console.log(line);
|
|
2340
|
+
}, claim));
|
|
2341
|
+
} catch (error) {
|
|
2342
|
+
const failure = (0, ownership_claim_exports.claimFailure)(error, opts.delete ? "stim gc --delete" : "stim gc --idle <duration>") ?? (deps.operation?.signal.aborted ? {
|
|
2343
|
+
code: "STIM_CANCELLED",
|
|
2344
|
+
message: "GC was cancelled.",
|
|
2345
|
+
remedy: "Retry the same GC command."
|
|
2346
|
+
} : null);
|
|
2347
|
+
if (!failure) throw error;
|
|
2348
|
+
console.error(`${failure.code}: ${failure.message}`);
|
|
2349
|
+
console.error(failure.remedy);
|
|
2350
|
+
result = {
|
|
2351
|
+
payload: failure,
|
|
2352
|
+
exitCode: 1
|
|
2353
|
+
};
|
|
2412
2354
|
}
|
|
2413
|
-
|
|
2355
|
+
if (result.exitCode) process.exitCode = result.exitCode;
|
|
2356
|
+
if (opts.json) console.log(JSON.stringify(result.payload));
|
|
2414
2357
|
}
|
|
2415
|
-
|
|
2416
|
-
|
|
2358
|
+
/** Runs GC under this home's exclusive maintenance claim, retaining it when owned cleanup is unresolved. */
|
|
2359
|
+
async function withGcMaintenanceClaim(opts, work) {
|
|
2360
|
+
if (!opts.delete && opts.idle === void 0) return work(void 0);
|
|
2417
2361
|
let claim;
|
|
2362
|
+
let retained = false;
|
|
2418
2363
|
try {
|
|
2419
2364
|
const root = maintenanceRunClaims();
|
|
2420
2365
|
const attempt = (0, ownership_claim_exports.tryAcquireClaim)({
|
|
2421
2366
|
root,
|
|
2422
2367
|
mode: "exclusive",
|
|
2423
2368
|
label: "maintenance",
|
|
2424
|
-
details: { trigger: "gc --delete" }
|
|
2369
|
+
details: { trigger: opts.delete ? "gc --delete" : "gc --idle" }
|
|
2425
2370
|
});
|
|
2426
2371
|
if (attempt.pending) (0, ownership_claim_exports.releaseClaim)(attempt.pending);
|
|
2427
2372
|
claim = attempt.acquired;
|
|
@@ -2434,27 +2379,58 @@ async function maintenanceGuardedSweep(opts, deps) {
|
|
|
2434
2379
|
reason: `held by pid ${holder?.owner.pid ?? "?"} since ${holder?.startedAt ?? "?"} (${String(holder?.details.trigger ?? "maintenance")})`
|
|
2435
2380
|
});
|
|
2436
2381
|
}
|
|
2437
|
-
return await
|
|
2438
|
-
} catch (
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2382
|
+
return await work(claim);
|
|
2383
|
+
} catch (cause) {
|
|
2384
|
+
if (claim && (cause instanceof WorkspaceProcessCleanupError || cause instanceof Error && "reportCleanup" in cause)) {
|
|
2385
|
+
retained = true;
|
|
2386
|
+
throw Object.assign(new WorkspaceProcessCleanupError([.../* @__PURE__ */ new Set([claim, ...cause instanceof WorkspaceProcessCleanupError ? cause.claims : []])], cause.message, "Verify GC and its owned children have stopped."), { reportCleanup: true });
|
|
2387
|
+
}
|
|
2388
|
+
throw cause;
|
|
2389
|
+
} finally {
|
|
2390
|
+
if (claim && !retained) (0, ownership_claim_exports.releaseClaim)(claim);
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
/** Executes one already-admitted sweep with separate progress, results and exit status. */
|
|
2394
|
+
async function executeGc(opts, deps, write, claim) {
|
|
2395
|
+
const results = createGcResults();
|
|
2396
|
+
let failed = false;
|
|
2397
|
+
const output = {
|
|
2398
|
+
log: (line) => write("stdout", line),
|
|
2399
|
+
error: (line) => write("stderr", line),
|
|
2400
|
+
record: results.record,
|
|
2401
|
+
fail: () => {
|
|
2402
|
+
failed = true;
|
|
2403
|
+
}
|
|
2404
|
+
};
|
|
2405
|
+
try {
|
|
2406
|
+
declareSpawnsOn(deps.operation, claim);
|
|
2407
|
+
return {
|
|
2408
|
+
payload: await sweep(opts, deps, {
|
|
2409
|
+
output,
|
|
2410
|
+
takeResults: results.take
|
|
2411
|
+
}),
|
|
2412
|
+
exitCode: failed ? 1 : 0
|
|
2413
|
+
};
|
|
2445
2414
|
} finally {
|
|
2446
|
-
|
|
2415
|
+
stopDeclaringSpawnsOn(deps.operation, claim);
|
|
2447
2416
|
}
|
|
2448
2417
|
}
|
|
2449
|
-
async function sweep(opts, deps) {
|
|
2418
|
+
async function sweep(opts, deps, execution, reportOnly = false) {
|
|
2419
|
+
const { output } = execution;
|
|
2420
|
+
const core = (dependencies) => runGcCore(opts, dependencies, execution, reportOnly);
|
|
2450
2421
|
if (opts.cache && (opts.worktrees || opts.idle !== void 0)) {
|
|
2451
2422
|
const flag = opts.worktrees ? "--worktrees" : "--idle";
|
|
2452
2423
|
const message = opts.worktrees ? "--cache acts only on the named caches, and --worktrees sweeps linked worktrees." : "--cache acts only on the named caches, and --idle shuts down idle owned devices.";
|
|
2453
2424
|
const remedy = `Run \`stim gc ${flag}${opts.worktrees ? "" : " <duration>"}\` and \`stim gc --cache <name>\` separately.`;
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2425
|
+
if (reportOnly) return {
|
|
2426
|
+
code: "STIM_BAD_ARG",
|
|
2427
|
+
message,
|
|
2428
|
+
remedy
|
|
2429
|
+
};
|
|
2430
|
+
output.error(chalk.red(message));
|
|
2431
|
+
output.error(chalk.dim(remedy));
|
|
2432
|
+
output.error(chalk.red("failed: STIM_BAD_ARG"));
|
|
2433
|
+
output.fail();
|
|
2458
2434
|
return {
|
|
2459
2435
|
code: "STIM_BAD_ARG",
|
|
2460
2436
|
message,
|
|
@@ -2465,10 +2441,15 @@ async function sweep(opts, deps) {
|
|
|
2465
2441
|
if (memoryKind && opts.olderThan !== void 0) {
|
|
2466
2442
|
const message = `--older-than does not apply to --cache ${memoryKind}, which acts on running processes.`;
|
|
2467
2443
|
const remedy = `Run \`stim gc --cache ${memoryKind}\` without --older-than.`;
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2444
|
+
if (reportOnly) return {
|
|
2445
|
+
code: "STIM_BAD_ARG",
|
|
2446
|
+
message,
|
|
2447
|
+
remedy
|
|
2448
|
+
};
|
|
2449
|
+
output.error(chalk.red(message));
|
|
2450
|
+
output.error(chalk.dim(remedy));
|
|
2451
|
+
output.error(chalk.red("failed: STIM_BAD_ARG"));
|
|
2452
|
+
output.fail();
|
|
2472
2453
|
return {
|
|
2473
2454
|
code: "STIM_BAD_ARG",
|
|
2474
2455
|
message,
|
|
@@ -2476,8 +2457,11 @@ async function sweep(opts, deps) {
|
|
|
2476
2457
|
};
|
|
2477
2458
|
}
|
|
2478
2459
|
const poolError = parkedMaxSetting("ios").error || parkedMaxSetting("android").error;
|
|
2479
|
-
if (poolError)
|
|
2480
|
-
|
|
2460
|
+
if (poolError) {
|
|
2461
|
+
if (reportOnly) phase("note", `${poolError} ${POOL_SETTING_REMEDY}`, deps.operation);
|
|
2462
|
+
else output.error(chalk.yellow(`${poolError} ${POOL_SETTING_REMEDY}`));
|
|
2463
|
+
}
|
|
2464
|
+
if (opts.cache) return core({
|
|
2481
2465
|
...deps,
|
|
2482
2466
|
precollectedEasSessionSweep: {
|
|
2483
2467
|
projectScope: null,
|
|
@@ -2488,8 +2472,8 @@ async function sweep(opts, deps) {
|
|
|
2488
2472
|
});
|
|
2489
2473
|
let projectRoot = null;
|
|
2490
2474
|
try {
|
|
2491
|
-
projectRoot = (deps.findProjectRoot ?? findProjectRoot)(process.cwd());
|
|
2492
|
-
if (!projectRoot) return
|
|
2475
|
+
projectRoot = (deps.findProjectRoot ?? findProjectRoot$1)(deps.operation?.cwd ?? process.cwd());
|
|
2476
|
+
if (!projectRoot) return core({
|
|
2493
2477
|
...deps,
|
|
2494
2478
|
precollectedEasSessionSweep: {
|
|
2495
2479
|
projectScope: null,
|
|
@@ -2498,7 +2482,7 @@ async function sweep(opts, deps) {
|
|
|
2498
2482
|
deletionSafe: false
|
|
2499
2483
|
}
|
|
2500
2484
|
});
|
|
2501
|
-
if (!(deps.detectIsExpo ?? detectIsExpo)(projectRoot)) return
|
|
2485
|
+
if (!(deps.detectIsExpo ?? detectIsExpo)(projectRoot)) return core({
|
|
2502
2486
|
...deps,
|
|
2503
2487
|
precollectedEasSessionSweep: {
|
|
2504
2488
|
projectScope: projectRoot,
|
|
@@ -2508,7 +2492,7 @@ async function sweep(opts, deps) {
|
|
|
2508
2492
|
}
|
|
2509
2493
|
});
|
|
2510
2494
|
} catch (error) {
|
|
2511
|
-
return
|
|
2495
|
+
return core({
|
|
2512
2496
|
...deps,
|
|
2513
2497
|
precollectedEasSessionSweep: {
|
|
2514
2498
|
projectScope: projectRoot,
|
|
@@ -2524,13 +2508,19 @@ async function sweep(opts, deps) {
|
|
|
2524
2508
|
try {
|
|
2525
2509
|
easSessionSweep = await withProjectLock(projectRoot, () => {
|
|
2526
2510
|
sweepStarted = true;
|
|
2527
|
-
return withRemoteSessionGcLocks(projectRoot, deps, (coordinatedDeps) =>
|
|
2511
|
+
return withRemoteSessionGcLocks(projectRoot, deps, (coordinatedDeps) => collectEasSessionSweep(loadConfig$1(), coordinatedDeps));
|
|
2528
2512
|
}, {
|
|
2529
2513
|
waitMs: 0,
|
|
2530
2514
|
ownerPurpose: "EAS orphan sweep",
|
|
2531
|
-
machineRoot: deps.easLedgerRoot
|
|
2515
|
+
machineRoot: deps.easLedgerRoot,
|
|
2516
|
+
...deps.operation ? {
|
|
2517
|
+
signal: deps.operation.signal,
|
|
2518
|
+
declareSpawnsFor: deps.operation
|
|
2519
|
+
} : {}
|
|
2532
2520
|
});
|
|
2533
2521
|
} catch (error) {
|
|
2522
|
+
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
2523
|
+
deps.operation?.signal.throwIfAborted();
|
|
2534
2524
|
easSessionSweep = {
|
|
2535
2525
|
projectScope: projectRoot,
|
|
2536
2526
|
orphaned: [],
|
|
@@ -2538,49 +2528,53 @@ async function sweep(opts, deps) {
|
|
|
2538
2528
|
deletionSafe: false
|
|
2539
2529
|
};
|
|
2540
2530
|
}
|
|
2541
|
-
return
|
|
2531
|
+
return core({
|
|
2542
2532
|
...deps,
|
|
2543
2533
|
precollectedEasSessionSweep: easSessionSweep
|
|
2544
2534
|
});
|
|
2545
2535
|
}
|
|
2546
|
-
async function pruneDeadProjects(deadProjects) {
|
|
2536
|
+
async function pruneDeadProjects(deadProjects, output, operation) {
|
|
2547
2537
|
let deleteFailures = 0;
|
|
2548
2538
|
for (const path of deadProjects) {
|
|
2539
|
+
operation?.signal.throwIfAborted();
|
|
2549
2540
|
if (existsSync$1(path) || !isOnMountedVolume(path)) {
|
|
2550
|
-
|
|
2551
|
-
|
|
2541
|
+
output.log(chalk.yellow(`Kept ${path}: its absence can no longer be confirmed.`));
|
|
2542
|
+
output.record("project", "kept", path, { detail: "its absence can no longer be confirmed" });
|
|
2552
2543
|
continue;
|
|
2553
2544
|
}
|
|
2554
2545
|
const result = await reclaimProject(path, {
|
|
2555
2546
|
archive: { removedBy: "gc" },
|
|
2556
|
-
stopRequester: { by: "stim gc --delete" }
|
|
2547
|
+
stopRequester: { by: "stim gc --delete" },
|
|
2548
|
+
operation
|
|
2557
2549
|
}).catch((error) => {
|
|
2550
|
+
if (error instanceof WorkspaceProcessCleanupError) throw error;
|
|
2551
|
+
operation?.signal.throwIfAborted();
|
|
2558
2552
|
deleteFailures++;
|
|
2559
|
-
|
|
2560
|
-
|
|
2553
|
+
output.log(chalk.red(`Could not prune ${path}; its registry entry was kept: ${error.message}`));
|
|
2554
|
+
output.record("project", "failed", path, { detail: `its registry entry was kept: ${error.message}` });
|
|
2561
2555
|
return null;
|
|
2562
2556
|
});
|
|
2563
2557
|
if (!result) continue;
|
|
2564
2558
|
if (result.keptEntry) {
|
|
2565
|
-
|
|
2566
|
-
|
|
2559
|
+
output.log(chalk.yellow(`Could not fully prune ${path}; its registry entry was kept.`));
|
|
2560
|
+
output.record("project", "kept", path, { detail: "it could not be fully pruned; its registry entry was kept" });
|
|
2567
2561
|
} else {
|
|
2568
|
-
|
|
2569
|
-
|
|
2562
|
+
output.log(chalk.green(`Pruned ${path}`));
|
|
2563
|
+
output.record("project", "done", path);
|
|
2570
2564
|
}
|
|
2571
|
-
for (const dir of result.removedWorkspaceDirs)
|
|
2565
|
+
for (const dir of result.removedWorkspaceDirs) output.log(chalk.dim(` removed workspace output ${dir}`));
|
|
2572
2566
|
for (const dir of result.failedWorkspaceDirs) {
|
|
2573
|
-
|
|
2574
|
-
|
|
2567
|
+
output.log(chalk.red(` could not remove workspace output ${dir}`));
|
|
2568
|
+
output.record("workspaceDirectory", "failed", dir, { detail: `could not remove it while pruning ${path}` });
|
|
2575
2569
|
deleteFailures += 1;
|
|
2576
2570
|
}
|
|
2577
|
-
if (result.killedPid)
|
|
2578
|
-
if (result.stoppedSession)
|
|
2579
|
-
if (result.stoppedTunnel)
|
|
2580
|
-
for (const name of result.deletedDevices)
|
|
2571
|
+
if (result.killedPid) output.log(chalk.dim(` killed orphaned Metro pid ${result.killedPid}`));
|
|
2572
|
+
if (result.stoppedSession) output.log(chalk.dim(` stopped remote session ${result.stoppedSession}`));
|
|
2573
|
+
if (result.stoppedTunnel) output.log(chalk.dim(` stopped ${result.stoppedTunnel} tunnel`));
|
|
2574
|
+
for (const name of result.deletedDevices) output.record("device", "done", name);
|
|
2581
2575
|
for (const s of result.skippedDevices) {
|
|
2582
|
-
|
|
2583
|
-
|
|
2576
|
+
output.log(chalk.yellow(` ${s.name}: ${s.reason}`));
|
|
2577
|
+
output.record("device", result.failedDevices.includes(s) ? "failed" : "kept", s.name, {
|
|
2584
2578
|
id: s.udid ?? null,
|
|
2585
2579
|
detail: s.reason
|
|
2586
2580
|
});
|
|
@@ -2589,13 +2583,18 @@ async function pruneDeadProjects(deadProjects) {
|
|
|
2589
2583
|
}
|
|
2590
2584
|
return deleteFailures;
|
|
2591
2585
|
}
|
|
2592
|
-
async function attachMaintenancePreview(report, deleting, cache) {
|
|
2593
|
-
if (!deleting) report.maintenance = await previewMaintenance({
|
|
2586
|
+
async function attachMaintenancePreview(report, deleting, cache, refresh, operation) {
|
|
2587
|
+
if (!deleting) report.maintenance = await previewMaintenance({
|
|
2588
|
+
devices: !cache,
|
|
2589
|
+
refresh,
|
|
2590
|
+
operation
|
|
2591
|
+
});
|
|
2594
2592
|
}
|
|
2595
2593
|
function scopedDryRunAction(cache) {
|
|
2596
2594
|
const memory = memoryCacheKind(cache);
|
|
2597
2595
|
if (memory === "watchman") return "remove the stale roots and shut down the unused watchman";
|
|
2598
2596
|
if (memory === "gradle-daemons") return "stop the idle daemons";
|
|
2597
|
+
if (includesGradleVersions(cache)) return "delete the Gradle versions marked deletable";
|
|
2599
2598
|
return includesParkedDevices(cache) ? "erase the parked devices" : "empty the caches";
|
|
2600
2599
|
}
|
|
2601
2600
|
function dryRunHint(cache, all, actionable, caches) {
|
|
@@ -2604,45 +2603,72 @@ function dryRunHint(cache, all, actionable, caches) {
|
|
|
2604
2603
|
if (caches) return "Pass --delete --cache all to empty the caches above, or --delete --older-than <days> to trim them.";
|
|
2605
2604
|
return null;
|
|
2606
2605
|
}
|
|
2607
|
-
function
|
|
2606
|
+
function scopedWorkPending(cache, report) {
|
|
2607
|
+
return memoryWorkPending(cache, report.memory) || gradleVersionsPending(report.gradleVersions);
|
|
2608
|
+
}
|
|
2609
|
+
async function pruneStatusCaches(report, output, signal) {
|
|
2608
2610
|
if (report.cacheScope !== null) return 0;
|
|
2609
|
-
return deleteStaleStatusCaches(collectStaleStatusCaches(listMountedVolumes()).stale);
|
|
2611
|
+
return deleteStaleStatusCaches(collectStaleStatusCaches(listMountedVolumes()).stale, output, signal);
|
|
2612
|
+
}
|
|
2613
|
+
async function reclaimParkedDevices(report, deps, output) {
|
|
2614
|
+
return report.cacheScope ? await eraseParkedDevices(report.parkedSims, report.parkedAvds, output, deps.operation) : await deleteParkedSims(report.parkedSims, deps, output) + await deleteParkedAvds(report.parkedAvds, output, deps.operation);
|
|
2610
2615
|
}
|
|
2611
|
-
function
|
|
2612
|
-
|
|
2616
|
+
function reportActionable(report) {
|
|
2617
|
+
const { deadProjects, invalidProjects, orphanedDevices, staleDevices, staleDeviceRecords, buildLocks, buildSlots, deviceLeases, easSessionSweep, caches, olderThan, all, cacheScope: cache } = report;
|
|
2618
|
+
return archiveWorkPending(report.archives, deadProjects.length + invalidProjects.length > 0 || report.orphanedWorkspaces.length > 0 || Boolean(report.workspaceOutputs?.workspaces.some((entry) => entry.willClear)) || report.workspaceLogs.some((entry) => entry.willTrim) || report.recordings.some((entry) => entry.willDelete && !entry.withWorkspace) || Boolean(report.tempLeftovers?.entries.some((entry) => entry.willDelete)) || Boolean(report.worktreeSweep?.worktrees.some((entry) => !entry.skipped)) || report.parkedSims.length > 0 || report.parkedAvds.length + report.parkedHostedDevices.filter((device) => device.listed !== false).length > 0 || orphanedDevices.length > 0 || staleDevices.length + report.finishedDevices.length > 0 || staleDeviceRecords.length > 0 || report.staleLedgerEntries.length + report.staleEasLedgerEntries.length > 0 || report.staleStatusCaches.length > 0 || buildLocks.stale.length > 0 || buildSlots.stale.length > 0 || deviceLeases.expired.length > 0 || easSessionSweep.orphaned.length > 0 || (olderThan !== null || all) && caches.length > 0 || scopedWorkPending(cache, report));
|
|
2613
2619
|
}
|
|
2614
|
-
async function runGcCore(opts, deps) {
|
|
2615
|
-
|
|
2620
|
+
async function runGcCore(opts, deps, execution, reportOnly = false) {
|
|
2621
|
+
const { output, takeResults } = execution;
|
|
2622
|
+
const signal = deps.operation?.signal;
|
|
2623
|
+
throwIfCancelled(signal, "GC");
|
|
2616
2624
|
const olderThan = typeof opts.olderThan === "number" ? opts.olderThan : null;
|
|
2617
2625
|
const cache = typeof opts.cache === "string" && opts.cache.trim() ? opts.cache : null;
|
|
2626
|
+
const refresh = opts.delete === true;
|
|
2618
2627
|
const report = await collectGcReport({
|
|
2619
2628
|
olderThan,
|
|
2620
2629
|
cache,
|
|
2621
|
-
worktrees: Boolean(opts.worktrees)
|
|
2630
|
+
worktrees: Boolean(opts.worktrees),
|
|
2631
|
+
refresh
|
|
2622
2632
|
}, deps);
|
|
2633
|
+
throwIfCancelled(signal, "GC");
|
|
2623
2634
|
if (cache && report.caches.length === 0 && report.workspaceOutputs === null && !selectsBeyondCaches(cache)) {
|
|
2624
|
-
const names = [...new Set(discoverCaches().map((c) => c.name))];
|
|
2635
|
+
const names = [...new Set(discoverCaches(deps.operation?.env ?? process.env, deps.operation?.cwd).map((c) => c.name))];
|
|
2625
2636
|
const message = `No shared cache carries "${cache}" in its name or directory.`;
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2637
|
+
if (!reportOnly) {
|
|
2638
|
+
output.log(chalk.yellow(message));
|
|
2639
|
+
if (names.length) output.log(chalk.dim(`Caches on this machine: ${names.join(", ")}`));
|
|
2640
|
+
if (opts.json) output.fail();
|
|
2641
|
+
}
|
|
2629
2642
|
return {
|
|
2630
2643
|
code: "STIM_BAD_ARG",
|
|
2631
2644
|
message,
|
|
2632
2645
|
remedy: names.length ? `Pass --cache all or a name from: ${names.join(", ")}.` : "No shared cache was found on this machine."
|
|
2633
2646
|
};
|
|
2634
2647
|
}
|
|
2635
|
-
await attachMaintenancePreview(report, opts.delete === true, cache);
|
|
2648
|
+
await attachMaintenancePreview(report, opts.delete === true, cache, refresh, deps.operation);
|
|
2636
2649
|
const refusal = archiveRefusal(report.archives);
|
|
2637
|
-
if (refusal !== null)
|
|
2650
|
+
if (refusal !== null) {
|
|
2651
|
+
if (!reportOnly) {
|
|
2652
|
+
output.fail();
|
|
2653
|
+
output.error(chalk.red(refusal.message));
|
|
2654
|
+
output.error(chalk.dim(refusal.remedy));
|
|
2655
|
+
output.error(chalk.red(`failed: ${refusal.code}`));
|
|
2656
|
+
}
|
|
2657
|
+
return refusal;
|
|
2658
|
+
}
|
|
2638
2659
|
const all = report.all;
|
|
2639
|
-
for (const line of formatGcReport(report))
|
|
2660
|
+
if (!reportOnly) for (const line of formatGcReport(report)) output.log(line);
|
|
2640
2661
|
const { deadProjects, invalidProjects, orphanedDevices, staleDevices, staleDeviceRecords, buildLocks, buildSlots, deviceLeases, easSessionSweep, caches } = report;
|
|
2641
|
-
const actionable =
|
|
2662
|
+
const actionable = reportActionable(report);
|
|
2642
2663
|
const idle = opts.idle ?? null;
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2664
|
+
throwIfCancelled(signal, "GC");
|
|
2665
|
+
const idleFailures = idle === null ? 0 : (await shutDownIdleDevices(report.idleDevices, idle, async () => deps.operation ? collectIdleDevicesAsync(loadConfig$1(), await listAllIosSimsAsync({
|
|
2666
|
+
timeoutMs: DEVICE_LIST_TIMEOUT_MS,
|
|
2667
|
+
operation: deps.operation,
|
|
2668
|
+
signal
|
|
2669
|
+
}), report.deadProjects, Date.now(), deps.operation.env, signal, deps.operation) : collectIdleDevices(loadConfig$1(), listAllIosSims({ timeoutMs: DEVICE_LIST_TIMEOUT_MS }), report.deadProjects), output, signal, deps.operation)).failures;
|
|
2670
|
+
if (idleFailures) output.fail();
|
|
2671
|
+
const inventory = opts.json && !opts.delete && idle === null && report.cacheScope === null ? await collectInventory(loadConfig$1(), deadProjects, deps.operation?.env ?? process.env, deps.operation) : null;
|
|
2646
2672
|
const payload = (failures) => ({
|
|
2647
2673
|
mode: opts.delete ? "delete" : "dry-run",
|
|
2648
2674
|
idle,
|
|
@@ -2653,42 +2679,70 @@ async function runGcCore(opts, deps) {
|
|
|
2653
2679
|
failures,
|
|
2654
2680
|
sections: gcReportSections(report),
|
|
2655
2681
|
...report.maintenance ? { maintenance: report.maintenance } : {},
|
|
2656
|
-
results:
|
|
2682
|
+
results: reportOnly ? [] : takeResults(),
|
|
2657
2683
|
inventory
|
|
2658
2684
|
});
|
|
2659
2685
|
if (!opts.delete) {
|
|
2660
2686
|
const hint = dryRunHint(cache, all, actionable, caches.length > 0);
|
|
2661
|
-
if (hint)
|
|
2687
|
+
if (hint && !reportOnly) output.log(chalk.dim(`\n${hint}`));
|
|
2662
2688
|
return payload(idle === null ? null : idleFailures);
|
|
2663
2689
|
}
|
|
2664
2690
|
let deleteFailures = idleFailures;
|
|
2665
|
-
|
|
2666
|
-
deleteFailures +=
|
|
2667
|
-
|
|
2668
|
-
deleteFailures +=
|
|
2691
|
+
throwIfCancelled(signal, "GC");
|
|
2692
|
+
deleteFailures += await deleteArchives(report.archives, output, signal);
|
|
2693
|
+
throwIfCancelled(signal, "GC");
|
|
2694
|
+
deleteFailures += report.workspaceOutputs ? (await clearWorkspaceOutputs(report.workspaceOutputs, {
|
|
2695
|
+
olderThan,
|
|
2696
|
+
signal,
|
|
2697
|
+
operation: deps.operation
|
|
2698
|
+
}, output)).failures : 0;
|
|
2699
|
+
throwIfCancelled(signal, "GC");
|
|
2700
|
+
deleteFailures += await trimWorkspaceLogs(report.workspaceLogs, output, deps.operation);
|
|
2701
|
+
throwIfCancelled(signal, "GC");
|
|
2702
|
+
deleteFailures += await deleteRecordings(report.recordings, {
|
|
2669
2703
|
whole: all,
|
|
2670
2704
|
olderThan,
|
|
2671
2705
|
now: Date.now()
|
|
2672
|
-
});
|
|
2673
|
-
|
|
2674
|
-
deleteFailures += await
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
deleteFailures += await
|
|
2679
|
-
|
|
2680
|
-
deleteFailures +=
|
|
2681
|
-
|
|
2682
|
-
|
|
2706
|
+
}, output, signal);
|
|
2707
|
+
throwIfCancelled(signal, "GC");
|
|
2708
|
+
deleteFailures += await reclaimParkedDevices(report, deps, output);
|
|
2709
|
+
throwIfCancelled(signal, "GC");
|
|
2710
|
+
deleteFailures += await deleteParkedHostedDevices(report.parkedHostedDevices, { operation: deps.operation }, output);
|
|
2711
|
+
throwIfCancelled(signal, "GC");
|
|
2712
|
+
deleteFailures += await reclaimMemory(cache, report.memory, output, deps.operation);
|
|
2713
|
+
throwIfCancelled(signal, "GC");
|
|
2714
|
+
deleteFailures += await deleteGradleVersions(report.gradleVersions, output, deps.operation);
|
|
2715
|
+
throwIfCancelled(signal, "GC");
|
|
2716
|
+
removeInvalidProjectEntries(invalidProjects, output, signal);
|
|
2717
|
+
throwIfCancelled(signal, "GC");
|
|
2718
|
+
deleteFailures += await pruneDeadProjects(deadProjects, output, deps.operation);
|
|
2719
|
+
throwIfCancelled(signal, "GC");
|
|
2720
|
+
deleteFailures += await deleteOrphanedWorkspaces(report.orphanedWorkspaces, output, deps.operation);
|
|
2721
|
+
throwIfCancelled(signal, "GC");
|
|
2722
|
+
deleteFailures += await deleteProjectDevices(orphanedDevices, staleDevices, staleDeviceRecords, output, deps.operation);
|
|
2723
|
+
throwIfCancelled(signal, "GC");
|
|
2724
|
+
deleteFailures += await deleteFinishedDevices(report, output, deps.operation);
|
|
2725
|
+
throwIfCancelled(signal, "GC");
|
|
2726
|
+
deleteFailures += await forgetStaleLedgerEntries(report.staleLedgerEntries, output, signal);
|
|
2727
|
+
throwIfCancelled(signal, "GC");
|
|
2728
|
+
deleteFailures += await forgetStaleEasLedgerEntries(report.staleEasLedgerEntries, listMountedVolumes(), deps, output, signal);
|
|
2729
|
+
throwIfCancelled(signal, "GC");
|
|
2730
|
+
deleteFailures += await pruneStatusCaches(report, output, signal);
|
|
2731
|
+
if (report.tempLeftovers) deleteFailures += (await deleteTempLeftovers({
|
|
2732
|
+
env: deps.operation?.env ?? process.env,
|
|
2733
|
+
signal: deps.operation?.signal,
|
|
2734
|
+
operation: deps.operation
|
|
2735
|
+
}, output)).failures;
|
|
2683
2736
|
for (const lock of buildLocks.stale) {
|
|
2737
|
+
throwIfCancelled(signal, "GC");
|
|
2684
2738
|
const cleared = (0, ownership_claim_exports.clearFreeClaimSet)({
|
|
2685
2739
|
root: lock.path,
|
|
2686
2740
|
label: `${lock.platform} build`
|
|
2687
2741
|
});
|
|
2688
2742
|
if (cleared.status === "held") continue;
|
|
2689
2743
|
if (cleared.status === "refused") {
|
|
2690
|
-
|
|
2691
|
-
|
|
2744
|
+
output.log(chalk.yellow(`Left the build lock at ${lock.path} alone: ${cleared.reason}`));
|
|
2745
|
+
output.record("buildLock", "kept", `${lock.platform} build lock`, {
|
|
2692
2746
|
id: lock.path,
|
|
2693
2747
|
detail: cleared.reason
|
|
2694
2748
|
});
|
|
@@ -2696,25 +2750,26 @@ async function runGcCore(opts, deps) {
|
|
|
2696
2750
|
}
|
|
2697
2751
|
if (cleared.status === "failed") {
|
|
2698
2752
|
deleteFailures++;
|
|
2699
|
-
|
|
2700
|
-
|
|
2753
|
+
output.log(chalk.red(`Failed to clear the build lock at ${lock.path}: ${cleared.reason}`));
|
|
2754
|
+
output.record("buildLock", "failed", `${lock.platform} build lock`, {
|
|
2701
2755
|
id: lock.path,
|
|
2702
2756
|
detail: cleared.reason
|
|
2703
2757
|
});
|
|
2704
2758
|
continue;
|
|
2705
2759
|
}
|
|
2706
|
-
|
|
2707
|
-
|
|
2760
|
+
output.record("buildLock", "done", `${lock.platform} build lock`, { id: lock.path });
|
|
2761
|
+
output.log(chalk.green(`Cleared the ${lock.platform} build lock left by pid ${lock.pid ?? "?"} (${lock.projectRoot || "unrecorded workspace"})`));
|
|
2708
2762
|
}
|
|
2709
2763
|
for (const slot of buildSlots.stale) {
|
|
2764
|
+
throwIfCancelled(signal, "GC");
|
|
2710
2765
|
const cleared = (0, ownership_claim_exports.clearFreeClaimSet)({
|
|
2711
2766
|
root: slot.path,
|
|
2712
2767
|
label: "build slot"
|
|
2713
2768
|
});
|
|
2714
2769
|
if (cleared.status === "held") continue;
|
|
2715
2770
|
if (cleared.status === "refused") {
|
|
2716
|
-
|
|
2717
|
-
|
|
2771
|
+
output.log(chalk.yellow(`Left the build slot at ${slot.path} alone: ${cleared.reason}`));
|
|
2772
|
+
output.record("buildSlot", "kept", `build slot ${slot.index ?? "?"}`, {
|
|
2718
2773
|
id: slot.path,
|
|
2719
2774
|
detail: cleared.reason
|
|
2720
2775
|
});
|
|
@@ -2722,47 +2777,53 @@ async function runGcCore(opts, deps) {
|
|
|
2722
2777
|
}
|
|
2723
2778
|
if (cleared.status === "failed") {
|
|
2724
2779
|
deleteFailures++;
|
|
2725
|
-
|
|
2726
|
-
|
|
2780
|
+
output.log(chalk.red(`Failed to clear the build slot at ${slot.path}: ${cleared.reason}`));
|
|
2781
|
+
output.record("buildSlot", "failed", `build slot ${slot.index ?? "?"}`, {
|
|
2727
2782
|
id: slot.path,
|
|
2728
2783
|
detail: cleared.reason
|
|
2729
2784
|
});
|
|
2730
2785
|
continue;
|
|
2731
2786
|
}
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
}
|
|
2735
|
-
for (const entry of deviceLeases.expired)
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2787
|
+
output.record("buildSlot", "done", `build slot ${slot.index ?? "?"}`, { id: slot.path });
|
|
2788
|
+
output.log(chalk.green(`Cleared build slot ${slot.index ?? "?"} left by pid ${slot.pid ?? "?"} (${slot.projectRoot || "unrecorded workspace"})`));
|
|
2789
|
+
}
|
|
2790
|
+
for (const entry of deviceLeases.expired) {
|
|
2791
|
+
throwIfCancelled(signal, "GC");
|
|
2792
|
+
try {
|
|
2793
|
+
if (removeExpiredLease(entry)) {
|
|
2794
|
+
output.record("deviceLease", "done", `${entry.platform} lease on ${entry.id ?? entry.name}`, { id: entry.path });
|
|
2795
|
+
output.log(chalk.green(`Cleared the expired ${entry.platform} device lease on ${entry.id ?? entry.name} (held by ${entry.lease?.holder ?? "an unrecorded workspace"})`));
|
|
2796
|
+
} else output.log(chalk.dim(`${entry.name} is no longer an expired lease; left alone.`));
|
|
2797
|
+
} catch (err) {
|
|
2798
|
+
deleteFailures++;
|
|
2799
|
+
output.log(chalk.red(`Failed to clear the device lease at ${entry.path}: ${err?.message || err}`));
|
|
2800
|
+
output.record("deviceLease", "failed", `${entry.platform} lease on ${entry.id ?? entry.name}`, {
|
|
2801
|
+
id: entry.path,
|
|
2802
|
+
detail: err?.message || String(err)
|
|
2803
|
+
});
|
|
2804
|
+
}
|
|
2747
2805
|
}
|
|
2748
|
-
|
|
2749
|
-
|
|
2806
|
+
throwIfCancelled(signal, "GC");
|
|
2807
|
+
deleteFailures += await deleteEasSessions(easSessionSweep, deps, output);
|
|
2808
|
+
throwIfCancelled(signal, "GC");
|
|
2809
|
+
if (report.worktreeSweep) deleteFailures += await removeWorktrees(report.worktreeSweep, { operation: deps.operation }, output);
|
|
2750
2810
|
if (deleteFailures) {
|
|
2751
|
-
|
|
2752
|
-
|
|
2811
|
+
output.log(chalk.red(`\n${deleteFailures} entr${deleteFailures === 1 ? "y" : "ies"} could not be deleted; see above.`));
|
|
2812
|
+
output.fail();
|
|
2753
2813
|
}
|
|
2754
|
-
|
|
2814
|
+
throwIfCancelled(signal, "GC");
|
|
2815
|
+
if (all) await emptyCaches(caches, output, signal, deps.operation);
|
|
2755
2816
|
else if (olderThan === null) {
|
|
2756
|
-
if (caches.length)
|
|
2757
|
-
} else trimCaches(caches, olderThan);
|
|
2817
|
+
if (caches.length) output.log(chalk.dim("Shared caches left alone: pass --cache all to empty them, or --older-than <days> to trim them."));
|
|
2818
|
+
} else await trimCaches(caches, olderThan, output, signal, deps.operation);
|
|
2758
2819
|
return payload(deleteFailures);
|
|
2759
2820
|
}
|
|
2760
2821
|
function gcCommand(program) {
|
|
2761
|
-
program.command("gc").description("Report what Stim has left behind: dead project entries, orphaned workspace directories, clean Stim-managed linked worktrees whose branch is merged or whose pull request was merged or closed, orphaned owned devices and EAS sessions, records of devices that no longer exist, build locks whose builder is gone, expired physical-device leases, the shared build caches, the build outputs and logs of each workspace,
|
|
2822
|
+
program.command("gc").description("Report what Stim has left behind: dead project entries, orphaned workspace directories, clean Stim-managed linked worktrees whose branch is merged or whose pull request was merged or closed, orphaned owned devices and EAS sessions, records of devices that no longer exist, build locks whose builder is gone, expired physical-device leases, the shared build caches, the build outputs and logs of each workspace, the memory of watchman and of the Gradle and Kotlin daemons, and recognized leftovers in TMPDIR. Reports by default; pass --delete to act.").option("--worktrees", "also remove clean, idle Stim-managed linked worktrees, not only merged ones: with --delete run `stim worktree remove` (never --force) on each; idle means unused for --older-than days, 7 without it").option("--delete", "actually prune the reported entries, reap the reported devices, clear the build outputs of every workspace not in use, and trim its Metro, client and device logs to their newest 8 MiB").option("--older-than <days>", "also reap owned devices whose workspace has not been used this long, trim shared cache entries nothing has used in that time and device recordings made before it, and clear workspace build outputs only for workspaces idle this long and parked devices only once parked this long", (v) => {
|
|
2762
2823
|
const n = parseInt(v, 10);
|
|
2763
2824
|
if (!Number.isFinite(n) || String(n) !== String(v).trim()) throw new InvalidArgumentError("must be a whole number of days, e.g. --older-than 30");
|
|
2764
2825
|
return n;
|
|
2765
|
-
}).option("--cache <name>", "act on the shared caches whose name or directory contains <name>, every cache, the workspace build outputs and the device recordings with --cache all, only the workspace build outputs with --cache workspaces, only the device recordings with --cache recordings, only the parked simulators and emulators with --cache parked, the watchman daemon and its stale roots with --cache watchman,
|
|
2826
|
+
}).option("--cache <name>", "act on the shared caches whose name or directory contains <name>, every cache, the workspace build outputs and the device recordings with --cache all, only the workspace build outputs with --cache workspaces, only the device recordings with --cache recordings, only the parked simulators and emulators with --cache parked, the watchman daemon and its stale roots with --cache watchman, the Gradle and Kotlin compile daemons with --cache gradle-daemons, or the Gradle version caches and wrapper distributions no registered project pins with --cache gradle-versions; archived, archived:<id>, archived-logs, archived-recordings and archived-agent select archived history explicitly, and the three kind selectors also take :<id> (archived-logs:<id>) to clear one kind of one archive; --cache all excludes archives. all, workspaces, recordings, parked, archived, archived:<id>, archived-logs, archived-recordings, archived-agent, their :<id> forms, watchman, gradle-daemons and gradle-versions are reserved names that never select a single cache, and --cache all never includes watchman, gradle-daemons or gradle-versions. With --delete they are emptied whole (a parked device is erased and stays parked, watchman loses its stale roots and shuts down only when no client uses it, and only daemons proven idle stop), which is the only way to clear an index-backed cache; add --older-than <days> to trim them by age instead. Archive ids, including the :<id> of a kind selector, ignore --older-than; archived and per-kind archive selectors without an id filter by removal age. Only those caches are reported; other devices and project entries are not inspected. Caches outside the config dir are refused while STIM_HOME is set.", (v) => {
|
|
2766
2827
|
if (!v.trim()) throw new InvalidArgumentError("must name a cache, e.g. --cache \"compilation cache\"");
|
|
2767
2828
|
return v;
|
|
2768
2829
|
}).option("--idle <duration>", "shut down (never delete) owned simulators and emulators that have had no driver, claim, or activity for at least <duration>, such as 30m, 2h, or 1d; acts without --delete", (v) => {
|
|
@@ -2770,8 +2831,9 @@ function gcCommand(program) {
|
|
|
2770
2831
|
if (ms === null) throw new InvalidArgumentError("must be a whole number of minutes, hours, or days, e.g. --idle 2h");
|
|
2771
2832
|
return ms;
|
|
2772
2833
|
}).option("--json", "print the report as JSON on stdout; every other line goes to stderr").action(async (opts) => {
|
|
2773
|
-
await runGc(opts);
|
|
2834
|
+
const { interrupted } = await runProcessOperation(process.cwd(), (operation) => withDrainingCancellation(operation, operation.signal, () => runGc(opts, { operation })));
|
|
2835
|
+
if (interrupted !== null) process.exitCode = interrupted;
|
|
2774
2836
|
});
|
|
2775
2837
|
}
|
|
2776
2838
|
//#endregion
|
|
2777
|
-
export {
|
|
2839
|
+
export { runGc as a, readGcReport as i, executeGc as n, withGcMaintenanceClaim as o, gcCommand as r, formatGcReport as s, collectGcReport as t };
|