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,38 +1,69 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { h as listBuildLocks } from "./device-lease-BX1sJQ2E.mjs";
|
|
5
|
-
import { T as workspaceName, r as readWorkspaceState$1, s as workspaceLastUsed, x as workspaceDir, y as workspaceAgentDeviceDir } from "./workspace-state-DJt_MMvf.mjs";
|
|
1
|
+
import { a as workspaceProcessLockError, c as declareSpawnsOn, f as stopDeclaringSpawnsOn, g as process_identity_exports, i as withWorkspaceProcessLock, o as workspaceProcessLockPath, r as WorkspaceProcessCleanupError, t as getExecutor, x as ownership_claim_exports } from "./exec-DEoAhcze.mjs";
|
|
2
|
+
import { d as workspaceDir, h as workspaceName$1, l as workspaceAgentDeviceDir } from "./paths-DQtTgsBf.mjs";
|
|
3
|
+
import { F as isOnMountedVolume, G as projectDeviceSlots, H as deviceSlotPlatforms, I as listMountedVolumes, L as measuredDirectorySizeAsync, O as withConfigLockAsync$1, P as formatBytes, _ as loadConfig$1, d as getConfigDir, g as isPathPrefix, j as acquireAvdClaim, p as getProject } from "./config-DlrsduwG.mjs";
|
|
6
4
|
import { t as createLineReader } from "./process-output-Fpl8PYi8.mjs";
|
|
7
|
-
import { a as formatElapsed, l as phaseLine } from "./command-output-
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { _ as
|
|
14
|
-
import { t as
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import { buildSlotsDir, hostedNativeRecords, isJsonObject, macosAppState, parseHostedNativePlacement, readJsonFile, readMacosRecord, requireAutomaticMachine, tryAcquireBuildSlotClaim, unreadableHostedNative } from "@stim-cli/core/state";
|
|
21
|
-
import { existsSync as existsSync$1, lstatSync as lstatSync$1, readdirSync as readdirSync$1, realpathSync as realpathSync$1, rmSync as rmSync$1, statSync as statSync$1 } from "fs";
|
|
5
|
+
import { a as formatElapsed, l as phaseLine } from "./command-output-gwpm28fu.mjs";
|
|
6
|
+
import { f as throwIfCancelled, t as runOperationFile, u as cancellableSleep } from "./operation-exec-BW0em61G.mjs";
|
|
7
|
+
import { l as withWorkspaceStateLockAsync, r as readWorkspaceState$1, u as workspaceLastUsed } from "./workspace-state-U08dEzhG.mjs";
|
|
8
|
+
import { Nt as repoRootAsync, ut as resolveTailscaleBinary } from "./settings-C7O8A6ZI.mjs";
|
|
9
|
+
import { a as macosRuntimeClaim, n as macosDir } from "./state-08I3hw3_.mjs";
|
|
10
|
+
import { c as pidExists, f as signalProcessTree, p as signalProcessTreeAsync } from "./metro-DiXtA3Ty.mjs";
|
|
11
|
+
import { _ as chromeProcessStateAsync, c as webClaimRoot, i as clearWebRecord, o as readWebRecord, p as webProfileDir, t as CDP_PORT_LABEL, u as webFacts, v as liveProfileHolder, y as removeSingletonFiles } from "./state--smjFSTL.mjs";
|
|
12
|
+
import { t as canonicalPath } from "./paths-BsJMoPP2.mjs";
|
|
13
|
+
import { N as listBuildLocks, d as readAgentDeviceRecords, t as agentDeviceDaemon } from "./activity-CMLmzp7o.mjs";
|
|
14
|
+
import { i as recordCreatedDevice, r as readCreatedDevices$1, t as forgetCreatedDevice } from "./created-devices-x9rN6bhs.mjs";
|
|
15
|
+
import { H as ownedAvdMatchesConfiguration, K as ownedAvdSystemImage, L as listOrphanedAvdDirectories, S as deleteAvd, V as ownedAvdDirectory, at as waitForAndroidEmulatorShutdown, et as resolveOwnedAvdSerialAsync, g as avdPathExists, gt as parkSim, j as listAdbDevicesAsync, lt as isStimOwnedAvd, mt as eraseParkedAfter, nt as shutdownAndroidEmulator, p as assertOwnedAvdStoppedAsync, st as wipeAvdUserData, v as avdStorageRoots, vt as readParked, yt as removeParkedAfter } from "./android-C7sVmyI9.mjs";
|
|
16
|
+
import { E as resolveOwnedIosSim, O as shutdownIosSim, S as renameIosSim, c as eraseIosSim, f as listAllIosSimsAsync, o as deleteIosSim, p as listIosDeviceTypesAsync, s as deleteParkedIosSim, v as parkedSimName, y as parseRuntimeVersion } from "./ios-hcBpnEGk.mjs";
|
|
17
|
+
import { r as releaseBrowserPort } from "./named-ports-DkEzOfzO.mjs";
|
|
22
18
|
import { createHash, randomUUID } from "node:crypto";
|
|
19
|
+
import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, watch, writeFileSync } from "node:fs";
|
|
20
|
+
import { isDeepStrictEqual } from "node:util";
|
|
23
21
|
import { homedir } from "node:os";
|
|
22
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
23
|
+
import { ambientContext, ambientEnv, ambientWrite } from "@stim-cli/core";
|
|
24
|
+
import { buildSlotsDir, hostedNativeRecords, isJsonObject, macosAppState, parseHostedNativePlacement, readJsonFile, readMacosRecord, requireAutomaticMachine, tryAcquireBuildSlotClaim, unreadableHostedNative } from "@stim-cli/core/state";
|
|
25
|
+
import { basename as basename$1, isAbsolute as isAbsolute$1, join as join$1, relative as relative$1 } from "path";
|
|
24
26
|
import chalk from "chalk";
|
|
25
|
-
import {
|
|
27
|
+
import { existsSync as existsSync$1, lstatSync as lstatSync$1, readdirSync as readdirSync$1, statSync as statSync$1 } from "fs";
|
|
28
|
+
import { lstat, readFile, readdir, rm, stat, unlink } from "node:fs/promises";
|
|
26
29
|
import { resolve4 } from "node:dns/promises";
|
|
27
30
|
import { request } from "node:https";
|
|
31
|
+
//#region src/commands/gc/results.ts
|
|
32
|
+
function createGcResults() {
|
|
33
|
+
const results = [];
|
|
34
|
+
return {
|
|
35
|
+
record(kind, status, label, { id = null, bytes = null, detail = null } = {}) {
|
|
36
|
+
results.push({
|
|
37
|
+
kind,
|
|
38
|
+
status,
|
|
39
|
+
label,
|
|
40
|
+
id,
|
|
41
|
+
bytes,
|
|
42
|
+
detail
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
take() {
|
|
46
|
+
return results.splice(0);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const detachedResults = createGcResults();
|
|
51
|
+
const recordGcResult = detachedResults.record;
|
|
52
|
+
const takeGcResults = detachedResults.take;
|
|
53
|
+
const GC_OUTPUT = {
|
|
54
|
+
log: (line) => console.log(line),
|
|
55
|
+
error: (line) => console.error(line),
|
|
56
|
+
record: recordGcResult,
|
|
57
|
+
fail: () => {
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
//#endregion
|
|
28
62
|
//#region src/engine/build-slots.ts
|
|
29
63
|
const SLOT_PREFIX = "slot-";
|
|
30
64
|
const SLOT_POLL_MS = 1e3;
|
|
31
65
|
const SLOT_PROGRESS_MS = 3e4;
|
|
32
66
|
const SLOT_CEILING_MS = 54e5;
|
|
33
|
-
function sleepAsync$1(ms) {
|
|
34
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
35
|
-
}
|
|
36
67
|
function toRecord(holder) {
|
|
37
68
|
const details = holder.details;
|
|
38
69
|
return {
|
|
@@ -56,7 +87,7 @@ function tryAcquireBuildSlot({ max, root = null, logFile = null }) {
|
|
|
56
87
|
}
|
|
57
88
|
});
|
|
58
89
|
if (!got) return null;
|
|
59
|
-
declareSpawnsOn(got.claim);
|
|
90
|
+
declareSpawnsOn(ambientContext(), got.claim);
|
|
60
91
|
return {
|
|
61
92
|
acquired: true,
|
|
62
93
|
path: got.path,
|
|
@@ -74,7 +105,11 @@ function tryAcquireBuildSlot({ max, root = null, logFile = null }) {
|
|
|
74
105
|
function slotWaitingLine({ max, elapsedMs }) {
|
|
75
106
|
return `${"build".padEnd(11)} waiting for a build slot (all ${max} in use, ${formatElapsed(elapsedMs)} elapsed) -- stim guide lifecycle concurrency`;
|
|
76
107
|
}
|
|
77
|
-
async function acquireBuildSlot({ max, root = null, logFile = null, now = Date.now, out = () => {}, waitingFor = () => {},
|
|
108
|
+
async function acquireBuildSlot({ max, root = null, logFile = null, now = Date.now, out = () => {}, waitingFor = () => {}, signal, sleep = (ms) => cancellableSleep(ms, {
|
|
109
|
+
signal,
|
|
110
|
+
what: "build slot"
|
|
111
|
+
}), intervalMs = SLOT_POLL_MS, progressMs = SLOT_PROGRESS_MS, ceilingMs = SLOT_CEILING_MS, automatic = false }) {
|
|
112
|
+
throwIfCancelled(signal, "build slot");
|
|
78
113
|
if (automatic) requireAutomaticMachine("build", "local");
|
|
79
114
|
if (!max || max <= 0) return {
|
|
80
115
|
acquired: true,
|
|
@@ -85,6 +120,7 @@ async function acquireBuildSlot({ max, root = null, logFile = null, now = Date.n
|
|
|
85
120
|
let waited = false;
|
|
86
121
|
try {
|
|
87
122
|
for (;;) {
|
|
123
|
+
throwIfCancelled(signal, "build slot");
|
|
88
124
|
if (automatic) requireAutomaticMachine("build", "local");
|
|
89
125
|
const got = tryAcquireBuildSlot({
|
|
90
126
|
max,
|
|
@@ -123,7 +159,7 @@ async function acquireBuildSlot({ max, root = null, logFile = null, now = Date.n
|
|
|
123
159
|
}
|
|
124
160
|
function releaseBuildSlot(handle) {
|
|
125
161
|
if (!handle || handle.unlimited) return false;
|
|
126
|
-
stopDeclaringSpawnsOn(handle.claim);
|
|
162
|
+
stopDeclaringSpawnsOn(ambientContext(), handle.claim);
|
|
127
163
|
return (0, ownership_claim_exports.releaseClaim)(handle.claim);
|
|
128
164
|
}
|
|
129
165
|
function listBuildSlots() {
|
|
@@ -162,59 +198,55 @@ function listBuildSlots() {
|
|
|
162
198
|
return slots;
|
|
163
199
|
}
|
|
164
200
|
//#endregion
|
|
165
|
-
//#region src/
|
|
166
|
-
function
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
const identity = inspectIdentity(identityRecord);
|
|
198
|
-
if (identity === "gone" || identity === "different") return {
|
|
199
|
-
status: "stale",
|
|
200
|
-
pid
|
|
201
|
-
};
|
|
202
|
-
if (identity !== "same") return {
|
|
203
|
-
status: "unverified",
|
|
204
|
-
pid,
|
|
205
|
-
reason: "the recorded process identity could not be verified"
|
|
206
|
-
};
|
|
201
|
+
//#region src/devices/report-only-usage.ts
|
|
202
|
+
function canonical$1(path) {
|
|
203
|
+
try {
|
|
204
|
+
return realpathSync.native(path);
|
|
205
|
+
} catch {
|
|
206
|
+
return resolve(path);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function parseDu(output) {
|
|
210
|
+
const sizes = /* @__PURE__ */ new Map();
|
|
211
|
+
for (const line of output.split("\n")) {
|
|
212
|
+
const match = /^(\d+)\t(.+?)\r?$/.exec(line);
|
|
213
|
+
if (match) sizes.set(resolve(match[2]), Number(match[1]) * 1024);
|
|
214
|
+
}
|
|
215
|
+
return sizes;
|
|
216
|
+
}
|
|
217
|
+
async function measureDu(args, operation) {
|
|
218
|
+
let output = "";
|
|
219
|
+
let complete = true;
|
|
220
|
+
try {
|
|
221
|
+
output = await runOperationFile(operation, getExecutor(), "du", args, {
|
|
222
|
+
timeoutMs: 2e4,
|
|
223
|
+
cwd: operation?.cwd,
|
|
224
|
+
signal: operation?.signal
|
|
225
|
+
});
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
228
|
+
operation?.signal.throwIfAborted();
|
|
229
|
+
complete = false;
|
|
230
|
+
const failure = error;
|
|
231
|
+
if (failure.status === 1 && !failure.code && typeof failure.stdout === "string") output = failure.stdout;
|
|
232
|
+
}
|
|
207
233
|
return {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
pid,
|
|
211
|
-
port,
|
|
212
|
-
mode: state?.mode ?? record?.mode ?? null,
|
|
213
|
-
startedAt: state?.startedAt ?? record?.startedAt ?? null
|
|
234
|
+
sizes: parseDu(output),
|
|
235
|
+
complete
|
|
214
236
|
};
|
|
215
237
|
}
|
|
216
|
-
function
|
|
217
|
-
|
|
238
|
+
function writeUsageCache(file, value, signal) {
|
|
239
|
+
signal?.throwIfAborted();
|
|
240
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
241
|
+
try {
|
|
242
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
243
|
+
writeFileSync(temporary, JSON.stringify(value));
|
|
244
|
+
renameSync(temporary, file);
|
|
245
|
+
} catch {
|
|
246
|
+
try {
|
|
247
|
+
rmSync(temporary, { force: true });
|
|
248
|
+
} catch {}
|
|
249
|
+
}
|
|
218
250
|
}
|
|
219
251
|
//#endregion
|
|
220
252
|
//#region src/engine/public-http-probe.ts
|
|
@@ -275,1282 +307,1552 @@ async function probePublicHttp(url, signal) {
|
|
|
275
307
|
});
|
|
276
308
|
}
|
|
277
309
|
//#endregion
|
|
278
|
-
//#region src/
|
|
279
|
-
function
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
]
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
"
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
]
|
|
296
|
-
};
|
|
297
|
-
return {
|
|
298
|
-
bin: "ngrok",
|
|
299
|
-
args: [
|
|
300
|
-
"http",
|
|
301
|
-
String(port),
|
|
302
|
-
`--log=${logFile || "stdout"}`,
|
|
303
|
-
"--log-format=json",
|
|
304
|
-
...ngrokUrl ? ["--url", ngrokUrl] : []
|
|
305
|
-
]
|
|
306
|
-
};
|
|
310
|
+
//#region src/devices/agent-device-cleanup.ts
|
|
311
|
+
function parseAgentDeviceSessions(output) {
|
|
312
|
+
const result = JSON.parse(output);
|
|
313
|
+
if (result?.success !== true || !Array.isArray(result.data?.sessions)) throw new Error("agent-device did not return a successful session list");
|
|
314
|
+
return result.data.sessions.flatMap((entry) => {
|
|
315
|
+
if (!entry || typeof entry !== "object") return [];
|
|
316
|
+
const row = entry;
|
|
317
|
+
const id = row.platform === "ios" ? row.device_udid : row.id;
|
|
318
|
+
if (row.platform !== "ios" && row.platform !== "android" || typeof row.name !== "string" || !row.name.trim() || typeof id !== "string" || !id || typeof row.createdAt !== "number" || !Number.isFinite(row.createdAt) || row.platform === "ios" && row.id !== void 0 && row.id !== id) return [];
|
|
319
|
+
return [{
|
|
320
|
+
name: typeof row.address === "string" && row.address.trim() ? row.address : row.name,
|
|
321
|
+
platform: row.platform,
|
|
322
|
+
id,
|
|
323
|
+
device: typeof row.device === "string" ? row.device : null,
|
|
324
|
+
createdAt: row.createdAt
|
|
325
|
+
}];
|
|
326
|
+
});
|
|
307
327
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
328
|
+
function isOwnDeviceSession(session, device, owner) {
|
|
329
|
+
if (session.platform !== device.platform || device.id !== null && session.id !== device.id) return false;
|
|
330
|
+
if (device.platform === "android" && session.device !== device.avdName.replaceAll("_", " ")) return false;
|
|
331
|
+
if (!owner) return true;
|
|
332
|
+
return owner.claims.some((claim) => claim.deviceId === session.id && claim.session === session.name && claim.workspace !== null && (within(owner.workspace, claim.workspace) || claim.workspace === owner.repoRoot));
|
|
312
333
|
}
|
|
313
|
-
function
|
|
314
|
-
|
|
334
|
+
function within(root, path) {
|
|
335
|
+
const rest = relative(root, path);
|
|
336
|
+
return rest === "" || !isAbsolute(rest) && rest.split(sep)[0] !== "..";
|
|
337
|
+
}
|
|
338
|
+
function canonical(path) {
|
|
315
339
|
try {
|
|
316
|
-
|
|
340
|
+
return realpathSync(path);
|
|
317
341
|
} catch {
|
|
318
342
|
return null;
|
|
319
343
|
}
|
|
320
|
-
if (typeof data !== "object" || data === null) return null;
|
|
321
|
-
const url = data.url;
|
|
322
|
-
return typeof url === "string" && /^https?:\/\//i.test(url) ? url : null;
|
|
323
|
-
}
|
|
324
|
-
function parseTailscaleLine(line) {
|
|
325
|
-
return /^https:\/\/[^\s/]+\.ts\.net(:\d+)?\/?$/.test(line) ? line : null;
|
|
326
|
-
}
|
|
327
|
-
function parserFor(provider) {
|
|
328
|
-
if (provider === "tailscale") return parseTailscaleLine;
|
|
329
|
-
return provider === "cloudflared" ? parseCloudflaredLine : parseNgrokLine;
|
|
330
|
-
}
|
|
331
|
-
const URL_TIMEOUT_MS = 15e3;
|
|
332
|
-
const REACHABLE_TIMEOUT_MS = 24e4;
|
|
333
|
-
const REACHABLE_POLL_MS = 2e3;
|
|
334
|
-
const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
335
|
-
async function withManagedTunnelLock(root, fn, options = {}) {
|
|
336
|
-
return withWorkspaceProcessLock(managedTunnelLockRoot(root), "metro-tunnel", fn, {
|
|
337
|
-
...options,
|
|
338
|
-
rejectOwnerPurposes: ["workspace removal"]
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
async function withManagedTunnelRemovalLock(root, fn, options = {}) {
|
|
342
|
-
return withWorkspaceProcessLock(managedTunnelLockRoot(root), "metro-tunnel", fn, {
|
|
343
|
-
...options,
|
|
344
|
-
ownerPurpose: "workspace removal"
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
async function withManagedRemoteWorktreeLock(worktreeRoot, fn, options = {}) {
|
|
348
|
-
return withWorkspaceProcessLock(managedRemoteWorktreeLockRoot(worktreeRoot), "managed-remote", fn, {
|
|
349
|
-
...options,
|
|
350
|
-
external: true,
|
|
351
|
-
rejectOwnerPurposes: ["worktree removal"]
|
|
352
|
-
});
|
|
353
344
|
}
|
|
354
|
-
|
|
355
|
-
return
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
});
|
|
345
|
+
function deviceClaims(device) {
|
|
346
|
+
return readAgentDeviceRecords(ambientEnv().HOME || ambientEnv().USERPROFILE || homedir(), ambientEnv()).filter((record) => record.kind === "claim" && (device.id === null || record.deviceId === device.id)).map((record) => ({
|
|
347
|
+
deviceId: record.deviceId,
|
|
348
|
+
session: record.session,
|
|
349
|
+
workspace: record.workspace && canonical(record.workspace)
|
|
350
|
+
}));
|
|
360
351
|
}
|
|
361
|
-
function
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
}];
|
|
369
|
-
const reasons = [];
|
|
370
|
-
for (const { label, path } of locks) {
|
|
371
|
-
const claims = (0, ownership_claim_exports.readClaimSet)(path);
|
|
372
|
-
const holder = claims.live[0];
|
|
373
|
-
if (holder) {
|
|
374
|
-
const purpose = typeof holder.details.purpose === "string" ? ` for ${holder.details.purpose}` : "";
|
|
375
|
-
reasons.push(`the ${label} lock is held${purpose}`);
|
|
376
|
-
} else if (claims.unresolved[0]) reasons.push(`the ${label} lock cannot be resolved: ${claims.unresolved[0].reason}`);
|
|
352
|
+
function explainFailure(error) {
|
|
353
|
+
const stdout = error?.stdout;
|
|
354
|
+
let failure;
|
|
355
|
+
try {
|
|
356
|
+
failure = typeof stdout === "string" ? JSON.parse(stdout)?.error : void 0;
|
|
357
|
+
} catch {
|
|
358
|
+
failure = void 0;
|
|
377
359
|
}
|
|
378
|
-
return
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
const key = createHash("sha256").update(resolve(root)).digest("hex");
|
|
382
|
-
return join(getConfigDir(), "process-locks", key);
|
|
383
|
-
}
|
|
384
|
-
function managedRemoteWorktreeLockRoot(worktreeRoot) {
|
|
385
|
-
const key = createHash("sha256").update(resolve(worktreeRoot)).digest("hex");
|
|
386
|
-
return join(getConfigDir(), "process-locks", "worktrees", key);
|
|
360
|
+
if (typeof failure?.message !== "string" || !failure.message) return error;
|
|
361
|
+
const reason = typeof failure.details?.reason === "string" ? ` (${failure.details.reason})` : "";
|
|
362
|
+
return /* @__PURE__ */ new Error(`agent-device refused: ${failure.message}${reason}`);
|
|
387
363
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
return status !== null && status > 0;
|
|
364
|
+
function report(message) {
|
|
365
|
+
ambientWrite("stderr", `${phaseLine("device", message)}\n`);
|
|
391
366
|
}
|
|
392
|
-
function
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
settled = true;
|
|
404
|
-
clearTimeout(timer);
|
|
405
|
-
child.stdout?.removeListener("data", onOut);
|
|
406
|
-
child.stderr?.removeListener("data", onErr);
|
|
407
|
-
child.removeListener("error", onError);
|
|
408
|
-
child.removeListener("exit", onExit);
|
|
409
|
-
resolve({
|
|
410
|
-
url,
|
|
411
|
-
exited,
|
|
412
|
-
lastLine
|
|
413
|
-
});
|
|
367
|
+
async function closeOwnedDeviceSessions(device, stillOwned, workspace, repoRoot, operation) {
|
|
368
|
+
const exec = getExecutor();
|
|
369
|
+
try {
|
|
370
|
+
operation?.signal.throwIfAborted();
|
|
371
|
+
if (!exec.findExecutable("agent-device")) return;
|
|
372
|
+
const root = workspace === void 0 ? void 0 : canonical(workspace);
|
|
373
|
+
if (root === null) return;
|
|
374
|
+
const owner = root === void 0 ? void 0 : {
|
|
375
|
+
workspace: root,
|
|
376
|
+
repoRoot: repoRoot && (canonical(repoRoot) ?? void 0),
|
|
377
|
+
claims: deviceClaims(device)
|
|
414
378
|
};
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
settled = true;
|
|
446
|
-
clearInterval(interval);
|
|
447
|
-
clearTimeout(timer);
|
|
448
|
-
child.removeListener("error", onError);
|
|
449
|
-
child.removeListener("exit", onExit);
|
|
450
|
-
resolve({
|
|
451
|
-
url,
|
|
452
|
-
exited
|
|
453
|
-
});
|
|
454
|
-
};
|
|
455
|
-
const probe = () => {
|
|
379
|
+
const stateDir = root === void 0 ? void 0 : workspaceAgentDeviceDir(root);
|
|
380
|
+
const stateDirs = [void 0, ...stateDir && !["none", "dead"].includes(agentDeviceDaemon(stateDir)) ? [stateDir] : []];
|
|
381
|
+
for (const dir of stateDirs) {
|
|
382
|
+
const deadline = Date.now() + 15e3;
|
|
383
|
+
const run = async (args) => {
|
|
384
|
+
const remaining = deadline - Date.now();
|
|
385
|
+
if (remaining <= 0) throw new Error("agent-device cleanup timed out");
|
|
386
|
+
try {
|
|
387
|
+
return await runOperationFile(operation, exec, "agent-device", [
|
|
388
|
+
...args,
|
|
389
|
+
...dir ? ["--state-dir", dir] : [],
|
|
390
|
+
"--json",
|
|
391
|
+
"--daemon-transport",
|
|
392
|
+
"socket"
|
|
393
|
+
], {
|
|
394
|
+
timeoutMs: Math.min(5e3, remaining),
|
|
395
|
+
killSignal: "SIGKILL"
|
|
396
|
+
});
|
|
397
|
+
} catch (error) {
|
|
398
|
+
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
399
|
+
operation?.signal.throwIfAborted();
|
|
400
|
+
throw explainFailure(error);
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
const list = async () => parseAgentDeviceSessions(await run([
|
|
404
|
+
"session",
|
|
405
|
+
"list",
|
|
406
|
+
"--session",
|
|
407
|
+
"stim-teardown-inventory"
|
|
408
|
+
]));
|
|
456
409
|
try {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
if (
|
|
410
|
+
const sessions = (await list()).filter((session) => isOwnDeviceSession(session, device, owner));
|
|
411
|
+
for (const session of sessions) try {
|
|
412
|
+
if (!(await list()).some((current) => current.name === session.name && current.platform === session.platform && current.id === session.id && current.createdAt === session.createdAt)) continue;
|
|
413
|
+
if (!await stillOwned(session.id)) continue;
|
|
414
|
+
if (JSON.parse(await run([
|
|
415
|
+
"close",
|
|
416
|
+
"--session",
|
|
417
|
+
session.name,
|
|
418
|
+
"--session-lock",
|
|
419
|
+
"reject",
|
|
420
|
+
"--platform",
|
|
421
|
+
device.platform,
|
|
422
|
+
device.platform === "ios" ? "--udid" : "--serial",
|
|
423
|
+
session.id
|
|
424
|
+
]))?.success !== true) throw new Error("agent-device did not confirm session close");
|
|
425
|
+
report(`closed agent-device session ${session.name} on ${session.id}`);
|
|
426
|
+
} catch (error) {
|
|
427
|
+
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
428
|
+
operation?.signal.throwIfAborted();
|
|
429
|
+
report(`could not close agent-device session ${session.name} on ${session.id}: ${error.message}`);
|
|
460
430
|
}
|
|
461
|
-
} catch {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
}
|
|
472
|
-
function createTunnelLogFile(provider) {
|
|
473
|
-
const dir = join(getConfigDir(), "tunnel-logs");
|
|
474
|
-
mkdirSync(dir, { recursive: true });
|
|
475
|
-
const path = join(dir, `${provider}-${randomUUID()}.log`);
|
|
476
|
-
closeSync(openSync(path, "wx", 384));
|
|
477
|
-
return path;
|
|
431
|
+
} catch (error) {
|
|
432
|
+
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
433
|
+
operation?.signal.throwIfAborted();
|
|
434
|
+
report(`could not list agent-device sessions for ${device.platform === "ios" ? device.id : device.id ?? device.avdName}: ${error.message}`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
} catch (error) {
|
|
438
|
+
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
439
|
+
operation?.signal.throwIfAborted();
|
|
440
|
+
report(`could not list agent-device sessions for ${device.platform === "ios" ? device.id : device.id ?? device.avdName}: ${error.message}`);
|
|
441
|
+
}
|
|
478
442
|
}
|
|
479
|
-
|
|
480
|
-
|
|
443
|
+
//#endregion
|
|
444
|
+
//#region src/devices/teardown.ts
|
|
445
|
+
async function teardownParkedAvd(avdName, { operation } = {}) {
|
|
481
446
|
try {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
447
|
+
operation?.signal.throwIfAborted();
|
|
448
|
+
return await removeParkedAfter("android", avdName, async () => {
|
|
449
|
+
const result = await teardownOwnedAvd(avdName, {
|
|
450
|
+
del: true,
|
|
451
|
+
owner: { pool: true },
|
|
452
|
+
operation
|
|
453
|
+
});
|
|
454
|
+
if (result.status !== "torn-down" && result.status !== "missing") throw new Error(result.reason);
|
|
455
|
+
}, operation) ? {
|
|
456
|
+
status: "torn-down",
|
|
457
|
+
label: avdName
|
|
458
|
+
} : {
|
|
459
|
+
status: "skipped",
|
|
460
|
+
kind: "not-parked",
|
|
461
|
+
reason: "emulator is no longer parked"
|
|
462
|
+
};
|
|
463
|
+
} catch (error) {
|
|
464
|
+
if (error instanceof WorkspaceProcessCleanupError) throw error;
|
|
465
|
+
operation?.signal.throwIfAborted();
|
|
466
|
+
return {
|
|
467
|
+
status: "failed",
|
|
468
|
+
reason: String(error?.message || error)
|
|
469
|
+
};
|
|
498
470
|
}
|
|
499
471
|
}
|
|
500
|
-
|
|
501
|
-
const CLEANUP_POLL_MS = 25;
|
|
502
|
-
async function startTunnel({ provider, port, spawnFn = null, urlTimeoutMs = URL_TIMEOUT_MS, reachableTimeoutMs = REACHABLE_TIMEOUT_MS, probeReachable = defaultProbeReachable, now = Date.now, sleep = defaultSleep, ngrokUrl = null, requireReachable = true, cleanupTimeoutMs = CLEANUP_TIMEOUT_MS, isChildAlive = pidExists, readProcessToken = process_identity_exports.captureProcessToken, logFile = null }) {
|
|
503
|
-
const spawn = spawnFn || ((cmd, args, opts) => getExecutor().spawn(cmd, args, opts));
|
|
504
|
-
const outputFile = provider === "tailscale" ? null : logFile || (spawnFn ? null : createTunnelLogFile(provider));
|
|
505
|
-
let child;
|
|
472
|
+
async function teardownParkedIosSim(udid, { label, deleteSim = deleteParkedIosSim, operation } = {}) {
|
|
506
473
|
try {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
detached: true
|
|
521
|
-
});
|
|
522
|
-
} catch (err) {
|
|
523
|
-
removeTunnelLogFile(outputFile);
|
|
474
|
+
operation?.signal.throwIfAborted();
|
|
475
|
+
const removed = await removeParkedAfter("ios", udid, async () => {
|
|
476
|
+
await closeOwnedDeviceSessions({
|
|
477
|
+
platform: "ios",
|
|
478
|
+
id: udid
|
|
479
|
+
}, async () => Boolean((await resolveOwnedIosSimNow(udid, operation)).sim), void 0, void 0, operation);
|
|
480
|
+
await deleteSim(udid, operation);
|
|
481
|
+
}, operation);
|
|
482
|
+
if (!removed) return {
|
|
483
|
+
status: "skipped",
|
|
484
|
+
kind: "not-parked",
|
|
485
|
+
reason: "simulator is no longer parked"
|
|
486
|
+
};
|
|
524
487
|
return {
|
|
525
|
-
|
|
526
|
-
|
|
488
|
+
status: "torn-down",
|
|
489
|
+
label: label ?? removed.name
|
|
527
490
|
};
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
});
|
|
533
|
-
const cleanup = async (alreadyExited = childExited) => {
|
|
534
|
-
const stopped = await terminateChild(child, {
|
|
535
|
-
alreadyExited,
|
|
536
|
-
timeoutMs: cleanupTimeoutMs,
|
|
537
|
-
now,
|
|
538
|
-
sleep,
|
|
539
|
-
isAlive: isChildAlive
|
|
540
|
-
});
|
|
541
|
-
if (stopped) removeTunnelLogFile(outputFile);
|
|
542
|
-
return stopped ? { status: "stopped" } : {
|
|
491
|
+
} catch (error) {
|
|
492
|
+
if (error instanceof WorkspaceProcessCleanupError) throw error;
|
|
493
|
+
operation?.signal.throwIfAborted();
|
|
494
|
+
return {
|
|
543
495
|
status: "failed",
|
|
544
|
-
reason:
|
|
545
|
-
};
|
|
546
|
-
};
|
|
547
|
-
const failAfterCleanup = async (reason, alreadyExited = childExited) => {
|
|
548
|
-
const stopped = await cleanup(alreadyExited);
|
|
549
|
-
return stopped.status === "stopped" ? {
|
|
550
|
-
failed: true,
|
|
551
|
-
reason
|
|
552
|
-
} : {
|
|
553
|
-
failed: true,
|
|
554
|
-
reason: `${reason} ${stopped.reason}`,
|
|
555
|
-
cleanupFailed: true
|
|
496
|
+
reason: String(error?.message || error)
|
|
556
497
|
};
|
|
557
|
-
};
|
|
558
|
-
const pid = child.pid;
|
|
559
|
-
if (!pid) return failAfterCleanup(`${provider} started but reported no pid.`);
|
|
560
|
-
const captureChildIdentity = () => {
|
|
561
|
-
try {
|
|
562
|
-
return readProcessToken(pid);
|
|
563
|
-
} catch {
|
|
564
|
-
return null;
|
|
565
|
-
}
|
|
566
|
-
};
|
|
567
|
-
const initialProcessToken = captureChildIdentity();
|
|
568
|
-
if (!initialProcessToken) return failAfterCleanup(`${provider} started but its process identity token could not be read.`);
|
|
569
|
-
const { url, exited, lastLine } = outputFile ? await waitForFileUrl(child, outputFile, parserFor(provider), urlTimeoutMs) : await waitForUrl(child, parserFor(provider), urlTimeoutMs);
|
|
570
|
-
if (!url) return failAfterCleanup(exited ? provider === "tailscale" && lastLine ? `${provider} exited before printing a tunnel URL: ${lastLine}` : `${provider} exited before printing a tunnel URL.` : `${provider} did not print a tunnel URL within ${urlTimeoutMs}ms.`, exited || childExited);
|
|
571
|
-
if (!outputFile) resumeChildPipes(child);
|
|
572
|
-
if (requireReachable) {
|
|
573
|
-
if (!await waitUntilReachable({
|
|
574
|
-
url,
|
|
575
|
-
now,
|
|
576
|
-
sleep,
|
|
577
|
-
probe: probeReachable,
|
|
578
|
-
timeoutMs: reachableTimeoutMs
|
|
579
|
-
})) return failAfterCleanup(`${url} did not become reachable within ${reachableTimeoutMs}ms; ${provider} may still be registering, or the network path is blocked.`);
|
|
580
498
|
}
|
|
581
|
-
const finalProcessToken = captureChildIdentity();
|
|
582
|
-
if (!finalProcessToken || finalProcessToken !== initialProcessToken) return failAfterCleanup(`${provider} process identity changed before its tunnel could be recorded.`);
|
|
583
|
-
if (!outputFile) unrefChildPipes(child);
|
|
584
|
-
child.unref?.();
|
|
585
|
-
if (childExited) return failAfterCleanup(`${provider} exited before its tunnel could be recorded.`, true);
|
|
586
|
-
return {
|
|
587
|
-
url,
|
|
588
|
-
pid,
|
|
589
|
-
processToken: initialProcessToken,
|
|
590
|
-
logFile: outputFile,
|
|
591
|
-
cleanup: () => cleanup()
|
|
592
|
-
};
|
|
593
499
|
}
|
|
594
|
-
async function
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
500
|
+
async function eraseParkedIosSim(udid, { label, operation } = {}) {
|
|
501
|
+
try {
|
|
502
|
+
operation?.signal.throwIfAborted();
|
|
503
|
+
const erased = await eraseParkedAfter("ios", udid, async (_record, eraseStarted) => {
|
|
504
|
+
await closeOwnedDeviceSessions({
|
|
505
|
+
platform: "ios",
|
|
506
|
+
id: udid
|
|
507
|
+
}, async () => Boolean((await resolveOwnedIosSimNow(udid, operation)).sim), void 0, void 0, operation);
|
|
508
|
+
const resolved = resolveOwnedIosSim(udid, await listAllIosSimsAsync({
|
|
509
|
+
signal: operation?.signal,
|
|
510
|
+
operation
|
|
511
|
+
}));
|
|
512
|
+
if (resolved.missing) throw new Error(`simulator ${udid} is not on this machine`);
|
|
513
|
+
if (resolved.notOwned) throw new Error(`simulator ${udid} is now named ${JSON.stringify(resolved.notOwned)}`);
|
|
514
|
+
const state = resolved.sim.state;
|
|
515
|
+
if (state !== "Shutdown") throw new Error(`simulator ${udid} is ${state}, not Shutdown; it was kept`);
|
|
516
|
+
operation?.signal.throwIfAborted();
|
|
517
|
+
eraseStarted();
|
|
518
|
+
await eraseIosSim(udid, operation);
|
|
519
|
+
}, operation);
|
|
520
|
+
if (!erased) return {
|
|
521
|
+
status: "skipped",
|
|
522
|
+
kind: "not-parked",
|
|
523
|
+
reason: "simulator is no longer parked"
|
|
606
524
|
};
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
525
|
+
return {
|
|
526
|
+
status: "torn-down",
|
|
527
|
+
label: label ?? erased.name
|
|
528
|
+
};
|
|
529
|
+
} catch (error) {
|
|
530
|
+
if (error instanceof WorkspaceProcessCleanupError) throw error;
|
|
531
|
+
operation?.signal.throwIfAborted();
|
|
532
|
+
return {
|
|
533
|
+
status: "failed",
|
|
534
|
+
reason: String(error?.message || error)
|
|
611
535
|
};
|
|
612
536
|
}
|
|
613
|
-
return {
|
|
614
|
-
failed: true,
|
|
615
|
-
reason: failures.join(" ") || "No managed tunnel provider was selected."
|
|
616
|
-
};
|
|
617
537
|
}
|
|
618
|
-
function
|
|
619
|
-
child.stdout?.destroy?.();
|
|
620
|
-
child.stderr?.destroy?.();
|
|
621
|
-
}
|
|
622
|
-
function unrefChildPipes(child) {
|
|
623
|
-
resumeChildPipes(child);
|
|
624
|
-
for (const stream of [child.stdout, child.stderr]) stream?.unref?.();
|
|
625
|
-
}
|
|
626
|
-
function resumeChildPipes(child) {
|
|
627
|
-
child.stdout?.resume?.();
|
|
628
|
-
child.stderr?.resume?.();
|
|
629
|
-
}
|
|
630
|
-
async function signalAndWaitForExit(child, signal, { timeoutMs, now, sleep, isAlive, platform }) {
|
|
631
|
-
let exited = false;
|
|
632
|
-
const onExit = () => {
|
|
633
|
-
exited = true;
|
|
634
|
-
};
|
|
635
|
-
child.once("exit", onExit);
|
|
538
|
+
async function eraseParkedAvd(avdName, { operation } = {}) {
|
|
636
539
|
try {
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
540
|
+
operation?.signal.throwIfAborted();
|
|
541
|
+
return await eraseParkedAfter("android", avdName, async (_record, eraseStarted) => {
|
|
542
|
+
if ((await resolveOwnedAvdSerialAsync(avdName, {
|
|
543
|
+
timeoutMs: AVD_INVENTORY_TIMEOUT_MS,
|
|
544
|
+
operation,
|
|
545
|
+
signal: operation?.signal
|
|
546
|
+
})).serial) throw new Error(`AVD ${avdName} is running; it was kept`);
|
|
547
|
+
const result = await teardownOwnedAvd(avdName, {
|
|
548
|
+
wipe: true,
|
|
549
|
+
owner: { pool: true },
|
|
550
|
+
onWipeStart: eraseStarted,
|
|
551
|
+
operation
|
|
552
|
+
});
|
|
553
|
+
if (result.status !== "torn-down") throw new Error(result.reason ?? `AVD ${avdName} is ${result.status}`);
|
|
554
|
+
}, operation) ? {
|
|
555
|
+
status: "torn-down",
|
|
556
|
+
label: avdName
|
|
557
|
+
} : {
|
|
558
|
+
status: "skipped",
|
|
559
|
+
kind: "not-parked",
|
|
560
|
+
reason: "emulator is no longer parked"
|
|
561
|
+
};
|
|
562
|
+
} catch (error) {
|
|
563
|
+
if (error instanceof WorkspaceProcessCleanupError) throw error;
|
|
564
|
+
operation?.signal.throwIfAborted();
|
|
565
|
+
return {
|
|
566
|
+
status: "failed",
|
|
567
|
+
reason: String(error?.message || error)
|
|
568
|
+
};
|
|
654
569
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
570
|
+
}
|
|
571
|
+
const IOS_SHUTDOWN_SETTLE_MS = 15e3;
|
|
572
|
+
const ADB_RELEASE_WAIT_MS = 3e3;
|
|
573
|
+
const AVD_INVENTORY_TIMEOUT_MS = 15e3;
|
|
574
|
+
async function repoRootAbove(workspace, operation) {
|
|
575
|
+
if (workspace === void 0 || !getExecutor().findExecutable("agent-device")) return void 0;
|
|
576
|
+
const root = await repoRootAsync(workspace, {
|
|
577
|
+
declareFor: operation,
|
|
578
|
+
env: operation?.env,
|
|
579
|
+
signal: operation?.signal
|
|
580
|
+
});
|
|
581
|
+
try {
|
|
582
|
+
return root && realpathSync(root) !== realpathSync(workspace) ? root : void 0;
|
|
583
|
+
} catch {
|
|
584
|
+
return;
|
|
664
585
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
586
|
+
}
|
|
587
|
+
const IOS_SHUTDOWN_POLL_MS = 250;
|
|
588
|
+
const IOS_SHUTDOWN_ATTEMPTS = 2;
|
|
589
|
+
async function resolveOwnedIosSimNow(udid, operation) {
|
|
590
|
+
operation?.signal.throwIfAborted();
|
|
591
|
+
const sims = await listAllIosSimsAsync({
|
|
592
|
+
operation,
|
|
593
|
+
signal: operation?.signal
|
|
671
594
|
});
|
|
672
|
-
|
|
673
|
-
return
|
|
595
|
+
operation?.signal.throwIfAborted();
|
|
596
|
+
return resolveOwnedIosSim(udid, sims);
|
|
674
597
|
}
|
|
675
|
-
function
|
|
676
|
-
|
|
598
|
+
async function settleIosSimShutdown(udid, { now = Date.now, sleep }, operation) {
|
|
599
|
+
let resolved = await resolveOwnedIosSimNow(udid, operation);
|
|
600
|
+
for (let attempt = 1;; attempt++) {
|
|
601
|
+
const deadline = now() + IOS_SHUTDOWN_SETTLE_MS;
|
|
602
|
+
while (resolved.sim && resolved.sim.state !== "Shutdown" && now() < deadline) {
|
|
603
|
+
await (sleep ? sleep(IOS_SHUTDOWN_POLL_MS) : cancellableSleep(IOS_SHUTDOWN_POLL_MS, {
|
|
604
|
+
signal: operation?.signal,
|
|
605
|
+
what: "simulator shutdown"
|
|
606
|
+
}));
|
|
607
|
+
resolved = await resolveOwnedIosSimNow(udid, operation);
|
|
608
|
+
}
|
|
609
|
+
if (!resolved.sim || resolved.sim.state === "Shutdown" || attempt === IOS_SHUTDOWN_ATTEMPTS) return resolved;
|
|
610
|
+
await shutdownIosSim(udid, operation);
|
|
611
|
+
resolved = await resolveOwnedIosSimNow(udid, operation);
|
|
612
|
+
}
|
|
677
613
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
function isEsrch(err) {
|
|
681
|
-
return err?.code === "ESRCH";
|
|
614
|
+
function iosShutdownFailureReason(udid, state) {
|
|
615
|
+
return `simulator ${udid} is still ${state} after ${IOS_SHUTDOWN_ATTEMPTS} shutdown attempts and ${IOS_SHUTDOWN_ATTEMPTS * IOS_SHUTDOWN_SETTLE_MS / 1e3}s of waiting`;
|
|
682
616
|
}
|
|
683
|
-
async function
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
617
|
+
async function parkOwnedIosSim(settled, udid, park, operation) {
|
|
618
|
+
if (settled.missing) throw new Error(`simulator ${udid} disappeared after shutdown`);
|
|
619
|
+
if (settled.notOwned) throw new Error(`simulator ${udid} is now named ${JSON.stringify(settled.notOwned)}`);
|
|
620
|
+
const sim = settled.sim;
|
|
621
|
+
if (sim.state !== "Shutdown") throw new Error(iosShutdownFailureReason(udid, sim.state));
|
|
622
|
+
const model = (await listIosDeviceTypesAsync({
|
|
623
|
+
operation,
|
|
624
|
+
signal: operation?.signal
|
|
625
|
+
})).find((d) => d.identifier === sim.deviceTypeIdentifier)?.name ?? null;
|
|
626
|
+
const runtime = parseRuntimeVersion(sim.runtime);
|
|
627
|
+
const name = parkedSimName(sim.udid, {
|
|
628
|
+
model,
|
|
629
|
+
runtime
|
|
630
|
+
});
|
|
631
|
+
await renameIosSim(sim.udid, name, operation);
|
|
632
|
+
const record = {
|
|
633
|
+
udid: sim.udid,
|
|
634
|
+
name,
|
|
635
|
+
deviceTypeIdentifier: sim.deviceTypeIdentifier,
|
|
636
|
+
runtimeIdentifier: sim.runtime,
|
|
637
|
+
parkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
638
|
+
simslimManaged: Boolean(park.simslimManaged),
|
|
639
|
+
...park.appId ? { bundleId: park.appId } : {},
|
|
640
|
+
...park.cacheKey ? { cacheKey: park.cacheKey } : {}
|
|
693
641
|
};
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
642
|
+
return {
|
|
643
|
+
record,
|
|
644
|
+
evicted: parkSim({
|
|
645
|
+
platform: "ios",
|
|
646
|
+
projectPath: park.projectPath,
|
|
647
|
+
slot: park.slot,
|
|
648
|
+
record,
|
|
649
|
+
max: park.max
|
|
650
|
+
})
|
|
699
651
|
};
|
|
652
|
+
}
|
|
653
|
+
async function teardownOwnedIosSim(udid, { del = false, label, park, workspace, shutdownClock = {}, operation } = {}) {
|
|
654
|
+
let parkFallback;
|
|
700
655
|
try {
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
656
|
+
const resolved = await resolveOwnedIosSimNow(udid, operation);
|
|
657
|
+
if (resolved.notOwned) return {
|
|
658
|
+
status: "skipped",
|
|
659
|
+
kind: "not-owned",
|
|
660
|
+
reason: `sim "${resolved.notOwned}" is not Stim-owned: Stim has no record of creating it`
|
|
661
|
+
};
|
|
662
|
+
if (resolved.missing) return { status: "missing" };
|
|
663
|
+
await closeOwnedDeviceSessions({
|
|
664
|
+
platform: "ios",
|
|
665
|
+
id: udid
|
|
666
|
+
}, async () => Boolean((await resolveOwnedIosSimNow(udid, operation)).sim), workspace, void 0, operation);
|
|
667
|
+
const current = await resolveOwnedIosSimNow(udid, operation);
|
|
668
|
+
if (current.missing) return { status: "missing" };
|
|
669
|
+
if (current.notOwned) return {
|
|
670
|
+
status: "skipped",
|
|
671
|
+
kind: "not-owned",
|
|
672
|
+
reason: "simulator ownership changed before shutdown"
|
|
673
|
+
};
|
|
674
|
+
await shutdownIosSim(udid, operation);
|
|
675
|
+
const sim = resolved.sim;
|
|
676
|
+
if (del && park && park.max > 0) try {
|
|
677
|
+
const { record, evicted } = await parkOwnedIosSim(await settleIosSimShutdown(udid, shutdownClock, operation), udid, park, operation);
|
|
678
|
+
const removed = [];
|
|
679
|
+
const failures = [];
|
|
680
|
+
for (const entry of evicted) {
|
|
681
|
+
const result = await teardownParkedIosSim(entry.udid, {
|
|
682
|
+
label: entry.name,
|
|
683
|
+
operation
|
|
684
|
+
});
|
|
685
|
+
if (result.status === "torn-down") removed.push({
|
|
686
|
+
udid: entry.udid,
|
|
687
|
+
name: entry.name
|
|
688
|
+
});
|
|
689
|
+
else if (result.status === "failed") failures.push(`could not delete evicted ${entry.name} (${entry.udid}): ${result.reason}`);
|
|
690
|
+
}
|
|
691
|
+
return {
|
|
692
|
+
status: "torn-down",
|
|
693
|
+
label: label ?? sim.name ?? udid,
|
|
694
|
+
parked: {
|
|
695
|
+
udid: record.udid,
|
|
696
|
+
name: record.name
|
|
697
|
+
},
|
|
698
|
+
evicted: removed,
|
|
699
|
+
...failures.length ? { evictionFailures: failures } : {}
|
|
700
|
+
};
|
|
701
|
+
} catch (e) {
|
|
702
|
+
if (e instanceof WorkspaceProcessCleanupError) throw e;
|
|
703
|
+
operation?.signal.throwIfAborted();
|
|
704
|
+
parkFallback = String(e?.message || e);
|
|
705
|
+
}
|
|
706
|
+
if (del) {
|
|
707
|
+
await deleteIosSim(udid, operation);
|
|
708
|
+
return {
|
|
709
|
+
status: "torn-down",
|
|
710
|
+
label: label ?? sim.name ?? udid,
|
|
711
|
+
...parkFallback ? { parkFallback } : {}
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
const settled = await settleIosSimShutdown(udid, shutdownClock, operation);
|
|
715
|
+
if (settled.sim && settled.sim.state !== "Shutdown") return {
|
|
704
716
|
status: "failed",
|
|
705
|
-
|
|
717
|
+
label: label ?? sim.name ?? udid,
|
|
718
|
+
reason: iosShutdownFailureReason(udid, settled.sim.state)
|
|
719
|
+
};
|
|
720
|
+
const root = sim.state === "Shutdown" ? void 0 : await repoRootAbove(workspace, operation);
|
|
721
|
+
if (root && settled.sim) await closeOwnedDeviceSessions({
|
|
722
|
+
platform: "ios",
|
|
723
|
+
id: udid
|
|
724
|
+
}, async () => (await resolveOwnedIosSimNow(udid, operation)).sim?.state === "Shutdown", workspace, root, operation);
|
|
725
|
+
return {
|
|
726
|
+
status: "torn-down",
|
|
727
|
+
label: label ?? sim.name ?? udid
|
|
728
|
+
};
|
|
729
|
+
} catch (e) {
|
|
730
|
+
if (e instanceof WorkspaceProcessCleanupError) throw e;
|
|
731
|
+
operation?.signal.throwIfAborted();
|
|
732
|
+
return {
|
|
733
|
+
status: "failed",
|
|
734
|
+
reason: String(e?.message || e),
|
|
735
|
+
...parkFallback ? { parkFallback } : {}
|
|
706
736
|
};
|
|
707
737
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
738
|
+
}
|
|
739
|
+
function assertAvdReferences(avdName, owner) {
|
|
740
|
+
const config = loadConfig$1();
|
|
741
|
+
if (!config) throw new Error("Cannot verify AVD references without a Stim config; the data was kept.");
|
|
742
|
+
if (owner && "projectPath" in owner && owner.expectedRecord && !isDeepStrictEqual(deviceSlotPlatforms(config.projects[owner.projectPath], owner.slot)?.android, owner.expectedRecord)) throw new Error(`AVD ${avdName} has a changed workspace record; its data was kept.`);
|
|
743
|
+
for (const [projectPath, project] of Object.entries(config.projects)) for (const { slot, platforms } of projectDeviceSlots(project)) {
|
|
744
|
+
if (platforms.android?.avdName?.toLowerCase() !== avdName.toLowerCase()) continue;
|
|
745
|
+
if (!owner || !("projectPath" in owner) || owner.projectPath !== projectPath || (owner.slot ?? "default") !== slot || !platforms.android.owned) throw new Error(`AVD ${avdName} is referenced by ${projectPath} (${slot}); its data was kept.`);
|
|
746
|
+
}
|
|
747
|
+
if (readParked("android", { config }).some((entry) => entry.name.toLowerCase() === avdName.toLowerCase()) && !(owner && "pool" in owner)) throw new Error(`AVD ${avdName} is in the emulator pool; its data was kept.`);
|
|
748
|
+
}
|
|
749
|
+
async function removeOrphanedAvdDirectory(candidate, owner, operation) {
|
|
750
|
+
operation?.signal.throwIfAborted();
|
|
751
|
+
const surveyed = listOrphanedAvdDirectories(candidate.name).find((entry) => entry.directory === candidate.directory);
|
|
752
|
+
if (!surveyed || surveyed.name !== candidate.name || surveyed.dev !== candidate.dev || surveyed.ino !== candidate.ino) throw new Error(`AVD data at ${candidate.directory} changed or is registered; it was kept.`);
|
|
753
|
+
await assertOwnedAvdStoppedAsync(candidate.name, operation, () => candidate.directory);
|
|
754
|
+
const detached = await withConfigLockAsync$1(() => {
|
|
755
|
+
const current = lstatSync(candidate.directory);
|
|
756
|
+
if (!current.isDirectory() || current.dev !== candidate.dev || current.ino !== candidate.ino) throw new Error(`AVD data at ${candidate.directory} changed or is registered; it was kept.`);
|
|
757
|
+
assertAvdReferences(candidate.name, owner);
|
|
758
|
+
const name = `stim-gc-${randomUUID()}`;
|
|
759
|
+
recordCreatedDevice("android", name);
|
|
760
|
+
const destination = join(dirname(candidate.directory), `${name}.avd`);
|
|
761
|
+
try {
|
|
762
|
+
renameSync(candidate.directory, destination);
|
|
763
|
+
} catch (error) {
|
|
764
|
+
forgetCreatedDevice("android", name);
|
|
765
|
+
throw error;
|
|
766
|
+
}
|
|
767
|
+
forgetCreatedDevice("android", candidate.name);
|
|
768
|
+
return destination;
|
|
769
|
+
}, operation?.signal);
|
|
770
|
+
const current = lstatSync(detached);
|
|
771
|
+
if (!current.isDirectory() || current.dev !== candidate.dev || current.ino !== candidate.ino) throw new Error(`Detached AVD data changed at ${detached}; it was kept.`);
|
|
772
|
+
operation?.signal.throwIfAborted();
|
|
773
|
+
try {
|
|
774
|
+
await rm(detached, { recursive: true });
|
|
775
|
+
} catch (error) {
|
|
776
|
+
throw new Error(`Could not remove AVD data at ${detached}; retry gc --delete: ${error.message}`, { cause: error });
|
|
777
|
+
}
|
|
778
|
+
forgetCreatedDevice("android", basename(detached, ".avd"));
|
|
779
|
+
}
|
|
780
|
+
async function teardownUnregisteredAvd(avdName, del, owner, surveyed, operation) {
|
|
781
|
+
operation?.signal.throwIfAborted();
|
|
782
|
+
const candidates = surveyed ? [surveyed] : listOrphanedAvdDirectories(avdName);
|
|
783
|
+
if (candidates.length === 0) {
|
|
784
|
+
if (avdStorageRoots().some((root) => avdPathExists(join(root, `${avdName}.ini`)) || avdPathExists(join(root, `${avdName}.avd`)))) throw new Error(`AVD ${avdName} is not listed but its storage remains; it was kept.`);
|
|
785
|
+
return { status: "missing" };
|
|
786
|
+
}
|
|
787
|
+
if (!del) return {
|
|
788
|
+
status: "skipped",
|
|
789
|
+
reason: "AVD registration is missing; its data was kept."
|
|
713
790
|
};
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
791
|
+
for (const candidate of candidates) {
|
|
792
|
+
if (candidate.name !== avdName) throw new Error(`AVD data does not match ${avdName}; it was kept.`);
|
|
793
|
+
await removeOrphanedAvdDirectory(candidate, owner, operation);
|
|
794
|
+
}
|
|
795
|
+
return {
|
|
796
|
+
status: "torn-down",
|
|
797
|
+
label: avdName
|
|
718
798
|
};
|
|
719
|
-
removeRecordedTunnelLogFile(record.logFile);
|
|
720
|
-
return { status: "stopped" };
|
|
721
799
|
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
800
|
+
async function teardownOwnedAvd(avdName, options = {}) {
|
|
801
|
+
let claim;
|
|
802
|
+
let retained = false;
|
|
803
|
+
const operation = options.operation;
|
|
725
804
|
try {
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
805
|
+
operation?.signal.throwIfAborted();
|
|
806
|
+
if (!isStimOwnedAvd(avdName)) return {
|
|
807
|
+
status: "skipped",
|
|
808
|
+
kind: "not-owned",
|
|
809
|
+
reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
|
|
810
|
+
};
|
|
811
|
+
claim = acquireAvdClaim(avdName);
|
|
812
|
+
if (operation) declareSpawnsOn(operation, claim);
|
|
813
|
+
const result = await teardownClaimedAvd(avdName, claim, options);
|
|
814
|
+
operation?.signal.throwIfAborted();
|
|
815
|
+
if (result.status === "torn-down" || result.status === "missing") options.onRemoved?.();
|
|
816
|
+
return result;
|
|
817
|
+
} catch (error) {
|
|
818
|
+
if (claim && (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error)) {
|
|
819
|
+
retained = true;
|
|
820
|
+
throw Object.assign(new WorkspaceProcessCleanupError([.../* @__PURE__ */ new Set([claim, ...error instanceof WorkspaceProcessCleanupError ? error.claims : []])], error.message, "Verify the AVD operation and its descendants have stopped."), { reportCleanup: true });
|
|
821
|
+
}
|
|
822
|
+
operation?.signal.throwIfAborted();
|
|
823
|
+
return {
|
|
824
|
+
status: "failed",
|
|
825
|
+
reason: String(error?.message || error)
|
|
826
|
+
};
|
|
827
|
+
} finally {
|
|
828
|
+
if (operation) stopDeclaringSpawnsOn(operation, claim);
|
|
829
|
+
if (!retained) (0, ownership_claim_exports.releaseClaim)(claim);
|
|
729
830
|
}
|
|
730
831
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
status,
|
|
738
|
-
label,
|
|
739
|
-
id,
|
|
740
|
-
bytes,
|
|
741
|
-
detail
|
|
832
|
+
async function serialFree(serial, timeoutMs = AVD_INVENTORY_TIMEOUT_MS, operation) {
|
|
833
|
+
operation?.signal.throwIfAborted();
|
|
834
|
+
const adb = await listAdbDevicesAsync({
|
|
835
|
+
timeoutMs,
|
|
836
|
+
operation,
|
|
837
|
+
signal: operation?.signal
|
|
742
838
|
});
|
|
839
|
+
operation?.signal.throwIfAborted();
|
|
840
|
+
return ![
|
|
841
|
+
...adb.emulators,
|
|
842
|
+
...adb.physical,
|
|
843
|
+
...adb.unhealthy
|
|
844
|
+
].some((entry) => entry.serial === serial);
|
|
743
845
|
}
|
|
744
|
-
function
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
if (row.platform !== "ios" && row.platform !== "android" || typeof row.name !== "string" || !row.name.trim() || typeof id !== "string" || !id || typeof row.createdAt !== "number" || !Number.isFinite(row.createdAt) || row.platform === "ios" && row.id !== void 0 && row.id !== id) return [];
|
|
757
|
-
return [{
|
|
758
|
-
name: typeof row.address === "string" && row.address.trim() ? row.address : row.name,
|
|
759
|
-
platform: row.platform,
|
|
760
|
-
id,
|
|
761
|
-
device: typeof row.device === "string" ? row.device : null,
|
|
762
|
-
createdAt: row.createdAt
|
|
763
|
-
}];
|
|
764
|
-
});
|
|
765
|
-
}
|
|
766
|
-
function isOwnDeviceSession(session, device, owner) {
|
|
767
|
-
if (session.platform !== device.platform || device.id !== null && session.id !== device.id) return false;
|
|
768
|
-
if (device.platform === "android" && session.device !== device.avdName.replaceAll("_", " ")) return false;
|
|
769
|
-
if (!owner) return true;
|
|
770
|
-
return owner.claims.some((claim) => claim.deviceId === session.id && claim.session === session.name && claim.workspace !== null && (within(owner.workspace, claim.workspace) || claim.workspace === owner.repoRoot));
|
|
771
|
-
}
|
|
772
|
-
function within(root, path) {
|
|
773
|
-
const rest = relative(root, path);
|
|
774
|
-
return rest === "" || !isAbsolute(rest) && rest.split(sep)[0] !== "..";
|
|
775
|
-
}
|
|
776
|
-
function canonical(path) {
|
|
777
|
-
try {
|
|
778
|
-
return realpathSync(path);
|
|
779
|
-
} catch {
|
|
780
|
-
return null;
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
function deviceClaims(device) {
|
|
784
|
-
return readAgentDeviceRecords(homedir()).filter((record) => record.kind === "claim" && (device.id === null || record.deviceId === device.id)).map((record) => ({
|
|
785
|
-
deviceId: record.deviceId,
|
|
786
|
-
session: record.session,
|
|
787
|
-
workspace: record.workspace && canonical(record.workspace)
|
|
788
|
-
}));
|
|
789
|
-
}
|
|
790
|
-
function explainFailure(error) {
|
|
791
|
-
const stdout = error?.stdout;
|
|
792
|
-
let failure;
|
|
793
|
-
try {
|
|
794
|
-
failure = typeof stdout === "string" ? JSON.parse(stdout)?.error : void 0;
|
|
795
|
-
} catch {
|
|
796
|
-
failure = void 0;
|
|
797
|
-
}
|
|
798
|
-
if (typeof failure?.message !== "string" || !failure.message) return error;
|
|
799
|
-
const reason = typeof failure.details?.reason === "string" ? ` (${failure.details.reason})` : "";
|
|
800
|
-
return /* @__PURE__ */ new Error(`agent-device refused: ${failure.message}${reason}`);
|
|
801
|
-
}
|
|
802
|
-
function report(message) {
|
|
803
|
-
process.stderr.write(`${phaseLine("device", message)}\n`);
|
|
804
|
-
}
|
|
805
|
-
function closeOwnedDeviceSessions(device, stillOwned, workspace, repoRoot) {
|
|
806
|
-
const exec = getExecutor();
|
|
846
|
+
async function teardownClaimedAvd(avdName, claim, { operation, del = false, wipe = false, park, owner, workspace, orphanedDirectory, onlyIfMissing = false, onWipeStart, waitForShutdown = waitForAndroidEmulatorShutdown, assertStopped = (name) => assertOwnedAvdStoppedAsync(name, operation), resolveAvd: resolve = (name) => resolveOwnedAvdSerialAsync(name, {
|
|
847
|
+
timeoutMs: AVD_INVENTORY_TIMEOUT_MS,
|
|
848
|
+
operation,
|
|
849
|
+
signal: operation?.signal
|
|
850
|
+
}) }) {
|
|
851
|
+
let parkFallback;
|
|
852
|
+
const resolveAvd = async (name) => {
|
|
853
|
+
operation?.signal.throwIfAborted();
|
|
854
|
+
const result = await resolve(name);
|
|
855
|
+
operation?.signal.throwIfAborted();
|
|
856
|
+
return result;
|
|
857
|
+
};
|
|
807
858
|
try {
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
859
|
+
operation?.signal.throwIfAborted();
|
|
860
|
+
if (del || wipe) await withConfigLockAsync$1(() => assertAvdReferences(avdName, owner), operation?.signal);
|
|
861
|
+
const resolved = await resolveAvd(avdName);
|
|
862
|
+
if (resolved.notOwned) return {
|
|
863
|
+
status: "skipped",
|
|
864
|
+
kind: "not-owned",
|
|
865
|
+
reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
|
|
815
866
|
};
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
867
|
+
if (resolved.missing) {
|
|
868
|
+
if (wipe) throw new Error(`AVD ${avdName} is not registered; its data was kept.`);
|
|
869
|
+
return await teardownUnregisteredAvd(avdName, del, owner, orphanedDirectory, operation);
|
|
870
|
+
}
|
|
871
|
+
if (onlyIfMissing) return {
|
|
872
|
+
status: "skipped",
|
|
873
|
+
reason: "AVD registration appeared; its record was kept."
|
|
874
|
+
};
|
|
875
|
+
if (orphanedDirectory) return {
|
|
876
|
+
status: "skipped",
|
|
877
|
+
reason: "AVD registration appeared; its data was kept."
|
|
878
|
+
};
|
|
879
|
+
const serial = resolved.serial;
|
|
880
|
+
const stillStopped = async (sessionSerial) => {
|
|
881
|
+
const current = await resolveAvd(avdName);
|
|
882
|
+
return !current.notOwned && !current.missing && !current.serial && await serialFree(sessionSerial, AVD_INVENTORY_TIMEOUT_MS, operation);
|
|
883
|
+
};
|
|
884
|
+
if (serial) {
|
|
885
|
+
const stillOwned = async () => {
|
|
886
|
+
const current = await resolveAvd(avdName);
|
|
887
|
+
return !current.notOwned && !current.missing && current.serial === serial;
|
|
888
|
+
};
|
|
889
|
+
await closeOwnedDeviceSessions({
|
|
890
|
+
platform: "android",
|
|
891
|
+
id: serial,
|
|
892
|
+
avdName
|
|
893
|
+
}, stillOwned, workspace, void 0, operation);
|
|
894
|
+
if (!await stillOwned()) throw new Error(`Owned AVD ${avdName} changed before shutdown; it was kept.`);
|
|
895
|
+
await waitForShutdown(avdName, async (timeoutMs) => {
|
|
896
|
+
if (!operation) (0, ownership_claim_exports.markClaimChildPending)(claim);
|
|
823
897
|
try {
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
"--json",
|
|
828
|
-
"--daemon-transport",
|
|
829
|
-
"socket"
|
|
830
|
-
], {
|
|
831
|
-
timeoutMs: Math.min(5e3, remaining),
|
|
832
|
-
killSignal: "SIGKILL"
|
|
833
|
-
});
|
|
834
|
-
} catch (error) {
|
|
835
|
-
throw explainFailure(error);
|
|
898
|
+
await shutdownAndroidEmulator(serial, timeoutMs, operation);
|
|
899
|
+
} finally {
|
|
900
|
+
if (!operation) (0, ownership_claim_exports.clearClaimChild)(claim);
|
|
836
901
|
}
|
|
902
|
+
}, { operation });
|
|
903
|
+
const root = await repoRootAbove(workspace, operation);
|
|
904
|
+
if (root) {
|
|
905
|
+
const deadline = Date.now() + ADB_RELEASE_WAIT_MS;
|
|
906
|
+
while (!await serialFree(serial, Math.max(1, deadline - Date.now()), operation) && Date.now() < deadline) await cancellableSleep(250, {
|
|
907
|
+
signal: operation?.signal,
|
|
908
|
+
what: "emulator release"
|
|
909
|
+
});
|
|
910
|
+
await closeOwnedDeviceSessions({
|
|
911
|
+
platform: "android",
|
|
912
|
+
id: serial,
|
|
913
|
+
avdName
|
|
914
|
+
}, stillStopped, workspace, root, operation);
|
|
915
|
+
}
|
|
916
|
+
} else {
|
|
917
|
+
await closeOwnedDeviceSessions({
|
|
918
|
+
platform: "android",
|
|
919
|
+
id: null,
|
|
920
|
+
avdName
|
|
921
|
+
}, stillStopped, workspace, void 0, operation);
|
|
922
|
+
await waitForShutdown(avdName, null, { operation });
|
|
923
|
+
}
|
|
924
|
+
if (wipe && !del) {
|
|
925
|
+
const current = await resolveAvd(avdName);
|
|
926
|
+
if (current.notOwned || current.missing) throw new Error(`AVD ${avdName} changed before its data was wiped.`);
|
|
927
|
+
if (current.serial) throw new Error(`Owned AVD ${avdName} started again before its data was wiped.`);
|
|
928
|
+
await assertStopped(avdName);
|
|
929
|
+
await withConfigLockAsync$1(() => assertAvdReferences(avdName, owner), operation?.signal);
|
|
930
|
+
operation?.signal.throwIfAborted();
|
|
931
|
+
const directory = ownedAvdDirectory(avdName);
|
|
932
|
+
if (!directory) throw new Error(`The data directory of AVD ${avdName} could not be resolved.`);
|
|
933
|
+
onWipeStart?.();
|
|
934
|
+
await wipeAvdUserData(directory, operation?.signal);
|
|
935
|
+
}
|
|
936
|
+
if (del) {
|
|
937
|
+
const current = await resolveAvd(avdName);
|
|
938
|
+
if (current.notOwned) return {
|
|
939
|
+
status: "skipped",
|
|
940
|
+
kind: "not-owned",
|
|
941
|
+
reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
|
|
837
942
|
};
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
943
|
+
if (current.missing) return await teardownUnregisteredAvd(avdName, del, owner, void 0, operation);
|
|
944
|
+
if (current.serial) throw new Error(`Owned AVD ${avdName} started again before deletion.`);
|
|
945
|
+
await assertStopped(avdName);
|
|
946
|
+
await withConfigLockAsync$1(() => assertAvdReferences(avdName, owner), operation?.signal);
|
|
947
|
+
operation?.signal.throwIfAborted();
|
|
948
|
+
if (park && park.max > 0) try {
|
|
949
|
+
const systemImage = ownedAvdSystemImage(avdName);
|
|
950
|
+
if (!systemImage || !park.configuration || !ownedAvdMatchesConfiguration(avdName, park.configuration)) throw new Error("the AVD has no verified creation configuration");
|
|
951
|
+
const evicted = parkSim({
|
|
952
|
+
platform: "android",
|
|
953
|
+
projectPath: park.projectPath,
|
|
954
|
+
slot: park.slot,
|
|
955
|
+
max: park.max,
|
|
956
|
+
record: {
|
|
957
|
+
udid: avdName,
|
|
958
|
+
name: avdName,
|
|
959
|
+
systemImage,
|
|
960
|
+
configuration: park.configuration,
|
|
961
|
+
parkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
962
|
+
...park.appId ? { packageName: park.appId } : {},
|
|
963
|
+
...park.cacheKey ? { cacheKey: park.cacheKey } : {}
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
const removed = [];
|
|
967
|
+
const failures = [];
|
|
968
|
+
for (const entry of evicted) {
|
|
969
|
+
const result = await teardownParkedAvd(entry.name, { operation });
|
|
970
|
+
if (result.status === "torn-down") removed.push({
|
|
971
|
+
udid: entry.name,
|
|
972
|
+
name: entry.name,
|
|
973
|
+
platform: "android"
|
|
974
|
+
});
|
|
975
|
+
else if (result.status === "failed") failures.push(`could not delete evicted ${entry.name}: ${result.reason}`);
|
|
863
976
|
}
|
|
977
|
+
return {
|
|
978
|
+
status: "torn-down",
|
|
979
|
+
label: avdName,
|
|
980
|
+
parked: {
|
|
981
|
+
udid: avdName,
|
|
982
|
+
name: avdName,
|
|
983
|
+
platform: "android"
|
|
984
|
+
},
|
|
985
|
+
evicted: removed,
|
|
986
|
+
...failures.length ? { evictionFailures: failures } : {}
|
|
987
|
+
};
|
|
864
988
|
} catch (error) {
|
|
865
|
-
|
|
989
|
+
if (error instanceof WorkspaceProcessCleanupError) throw error;
|
|
990
|
+
operation?.signal.throwIfAborted();
|
|
991
|
+
parkFallback = String(error?.message || error);
|
|
992
|
+
}
|
|
993
|
+
const directory = ownedAvdDirectory(avdName);
|
|
994
|
+
const paths = avdStorageRoots().map((root) => join(root, `${avdName}.ini`));
|
|
995
|
+
if (directory) paths.push(directory);
|
|
996
|
+
if (!operation) (0, ownership_claim_exports.markClaimChildPending)(claim);
|
|
997
|
+
try {
|
|
998
|
+
await deleteAvd(avdName, operation);
|
|
999
|
+
} finally {
|
|
1000
|
+
if (!operation) (0, ownership_claim_exports.clearClaimChild)(claim);
|
|
866
1001
|
}
|
|
1002
|
+
const remaining = paths.filter(avdPathExists);
|
|
1003
|
+
if (remaining.length) throw new Error(`AVD ${avdName} was only partially deleted; data remains at ${remaining.join(", ")}.`);
|
|
867
1004
|
}
|
|
868
|
-
|
|
869
|
-
|
|
1005
|
+
return {
|
|
1006
|
+
status: "torn-down",
|
|
1007
|
+
label: avdName,
|
|
1008
|
+
serial: resolved.serial ?? null,
|
|
1009
|
+
...parkFallback ? { parkFallback } : {}
|
|
1010
|
+
};
|
|
1011
|
+
} catch (e) {
|
|
1012
|
+
if (e instanceof WorkspaceProcessCleanupError) throw e;
|
|
1013
|
+
operation?.signal.throwIfAborted();
|
|
1014
|
+
return {
|
|
1015
|
+
status: "failed",
|
|
1016
|
+
reason: String(e?.message || e)
|
|
1017
|
+
};
|
|
870
1018
|
}
|
|
871
1019
|
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
function
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1020
|
+
const BROWSER_EXIT_WAIT_MS = 15e3;
|
|
1021
|
+
const CHROME_KILL_WAIT_MS = 5e3;
|
|
1022
|
+
async function stopOwnedChrome(record, profile, operation) {
|
|
1023
|
+
for (const signal of ["SIGTERM", "SIGKILL"]) {
|
|
1024
|
+
operation?.signal.throwIfAborted();
|
|
1025
|
+
const state = await chromeProcessStateAsync(record, profile, operation);
|
|
1026
|
+
if (state === "gone") return true;
|
|
1027
|
+
if (state === "unknown") return false;
|
|
1028
|
+
try {
|
|
1029
|
+
await signalProcessTreeAsync(record.pid, signal, {
|
|
1030
|
+
group: true,
|
|
1031
|
+
operation
|
|
880
1032
|
});
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
};
|
|
1033
|
+
} catch (error) {
|
|
1034
|
+
if (error instanceof WorkspaceProcessCleanupError) throw error;
|
|
1035
|
+
operation?.signal.throwIfAborted();
|
|
1036
|
+
}
|
|
1037
|
+
const deadline = Date.now() + CHROME_KILL_WAIT_MS;
|
|
1038
|
+
while (await chromeProcessStateAsync(record, profile, operation) !== "gone" && Date.now() < deadline) await cancellableSleep(50, {
|
|
1039
|
+
signal: operation?.signal,
|
|
1040
|
+
what: "Chrome shutdown"
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
return await chromeProcessStateAsync(record, profile, operation) === "gone";
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Closes the workspace's Stim-owned Chrome and its supervisor, after verifying both process identities. With
|
|
1047
|
+
* `deleteProfile`, it also deletes the profile directory, only when the created-devices ledger lists it.
|
|
1048
|
+
*/
|
|
1049
|
+
async function teardownOwnedBrowser(root, options = {}) {
|
|
1050
|
+
try {
|
|
1051
|
+
return await withWorkspaceProcessLock(workspaceDir(root), "web", () => teardownBrowserHeld(root, options), {
|
|
1052
|
+
external: true,
|
|
1053
|
+
ownerPurpose: "browser teardown",
|
|
1054
|
+
signal: options.operation?.signal,
|
|
1055
|
+
declareSpawnsFor: options.operation
|
|
1056
|
+
});
|
|
890
1057
|
} catch (error) {
|
|
1058
|
+
if (error instanceof WorkspaceProcessCleanupError) throw error;
|
|
1059
|
+
options.operation?.signal.throwIfAborted();
|
|
891
1060
|
return {
|
|
892
1061
|
status: "failed",
|
|
893
1062
|
reason: String(error?.message || error)
|
|
894
1063
|
};
|
|
895
1064
|
}
|
|
896
1065
|
}
|
|
897
|
-
|
|
1066
|
+
/** {@link teardownOwnedBrowser} for a caller that already holds the {@link BROWSER_LOCK} lock. */
|
|
1067
|
+
async function teardownBrowserHeld(root, { deleteProfile = false, operation } = {}) {
|
|
1068
|
+
const signal = operation?.signal;
|
|
898
1069
|
try {
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1070
|
+
signal?.throwIfAborted();
|
|
1071
|
+
const record = readWebRecord(root);
|
|
1072
|
+
const profile = record?.profile ?? webProfileDir(root);
|
|
1073
|
+
if (record) {
|
|
1074
|
+
const supervisor = (0, process_identity_exports.inspectProcessIdentity)(record);
|
|
1075
|
+
const chrome = record.chromeProcess ? (0, process_identity_exports.inspectProcessIdentity)(record.chromeProcess) : "gone";
|
|
1076
|
+
if (supervisor === "unknown" || chrome === "unknown") return {
|
|
1077
|
+
status: "skipped",
|
|
1078
|
+
kind: "not-verified",
|
|
1079
|
+
reason: `the browser supervisor (pid ${record.pid}) or its Chrome could not be verified; its record was kept`
|
|
1080
|
+
};
|
|
1081
|
+
if (supervisor === "same") {
|
|
1082
|
+
try {
|
|
1083
|
+
process.kill(record.pid, "SIGTERM");
|
|
1084
|
+
} catch {}
|
|
1085
|
+
if (!await (0, process_identity_exports.waitForProcessExit)(record, BROWSER_EXIT_WAIT_MS, signal)) return {
|
|
1086
|
+
status: "failed",
|
|
1087
|
+
reason: `browser supervisor pid ${record.pid} did not exit`
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
if (record.chromeProcess && !await stopOwnedChrome(record.chromeProcess, profile, operation)) return {
|
|
1091
|
+
status: "failed",
|
|
1092
|
+
reason: `Chrome pid ${record.chromeProcess.pid} did not exit`
|
|
1093
|
+
};
|
|
1094
|
+
const exited = record.chromeProcess;
|
|
1095
|
+
const holder = liveProfileHolder(profile, { alive: (pid) => pidExists(pid) && !(pid === exited?.pid && (0, process_identity_exports.inspectProcessIdentity)(exited) === "gone") });
|
|
1096
|
+
if (holder !== null) return {
|
|
1097
|
+
status: "skipped",
|
|
1098
|
+
kind: "not-verified",
|
|
1099
|
+
reason: `pid ${holder} still holds ${profile}, and the browser record does not name it; it was left running`
|
|
1100
|
+
};
|
|
1101
|
+
const claims = (0, ownership_claim_exports.clearFreeClaimSet)({
|
|
1102
|
+
root: webClaimRoot(root),
|
|
1103
|
+
label: "browser supervisor"
|
|
1104
|
+
});
|
|
1105
|
+
if (claims.status !== "cleared") return {
|
|
1106
|
+
status: "skipped",
|
|
1107
|
+
kind: "not-verified",
|
|
1108
|
+
reason: claims.status === "held" ? `the browser supervisor claim is still held${claims.holder ? ` by pid ${claims.holder.owner.pid}` : ""}` : `the browser supervisor claim could not be cleared (${claims.reason}); remove it with ${(0, ownership_claim_exports.claimRemoveCommand)(webClaimRoot(root))}`
|
|
1109
|
+
};
|
|
1110
|
+
await withWorkspaceStateLockAsync(root, () => clearWebRecord(root, record), signal);
|
|
1111
|
+
await releaseBrowserPort(root, record.cdpPort, signal);
|
|
1112
|
+
} else {
|
|
1113
|
+
const holder = liveProfileHolder(profile);
|
|
1114
|
+
if (holder !== null) return {
|
|
1115
|
+
status: "skipped",
|
|
1116
|
+
kind: "not-verified",
|
|
1117
|
+
reason: `pid ${holder} holds ${profile} but no browser supervisor is recorded; it was left running`
|
|
1118
|
+
};
|
|
1119
|
+
const port = getProject(root)?.ports?.[CDP_PORT_LABEL];
|
|
1120
|
+
if (typeof port === "number") await releaseBrowserPort(root, port, signal);
|
|
1121
|
+
}
|
|
1122
|
+
signal?.throwIfAborted();
|
|
1123
|
+
if (existsSync(profile)) removeSingletonFiles(profile);
|
|
1124
|
+
if (deleteProfile && existsSync(profile)) {
|
|
1125
|
+
if (!readCreatedDevices$1().web.has(profile)) return {
|
|
1126
|
+
status: "skipped",
|
|
1127
|
+
kind: "not-owned",
|
|
1128
|
+
reason: `${profile} is not in Stim's ledger; it was kept`
|
|
1129
|
+
};
|
|
1130
|
+
await rm(profile, {
|
|
1131
|
+
recursive: true,
|
|
1132
|
+
force: true
|
|
1133
|
+
});
|
|
1134
|
+
signal?.throwIfAborted();
|
|
1135
|
+
forgetCreatedDevice("web", profile);
|
|
1136
|
+
}
|
|
1137
|
+
return record ? {
|
|
912
1138
|
status: "torn-down",
|
|
913
|
-
label:
|
|
914
|
-
};
|
|
1139
|
+
label: `Chrome (${profile})`
|
|
1140
|
+
} : { status: "missing" };
|
|
915
1141
|
} catch (error) {
|
|
1142
|
+
if (error instanceof WorkspaceProcessCleanupError) throw error;
|
|
1143
|
+
signal?.throwIfAborted();
|
|
916
1144
|
return {
|
|
917
1145
|
status: "failed",
|
|
918
1146
|
reason: String(error?.message || error)
|
|
919
1147
|
};
|
|
920
1148
|
}
|
|
921
1149
|
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
const resolved = resolveOwnedIosSim(udid);
|
|
930
|
-
if (resolved.missing) throw new Error(`simulator ${udid} is not on this machine`);
|
|
931
|
-
if (resolved.notOwned) throw new Error(`simulator ${udid} is now named ${JSON.stringify(resolved.notOwned)}`);
|
|
932
|
-
const state = resolved.sim.state;
|
|
933
|
-
if (state !== "Shutdown") throw new Error(`simulator ${udid} is ${state}, not Shutdown; it was kept`);
|
|
934
|
-
eraseStarted();
|
|
935
|
-
eraseIosSim(udid);
|
|
936
|
-
});
|
|
937
|
-
if (!erased) return {
|
|
938
|
-
status: "skipped",
|
|
939
|
-
kind: "not-parked",
|
|
940
|
-
reason: "simulator is no longer parked"
|
|
941
|
-
};
|
|
942
|
-
return {
|
|
943
|
-
status: "torn-down",
|
|
944
|
-
label: label ?? erased.name
|
|
945
|
-
};
|
|
946
|
-
} catch (error) {
|
|
947
|
-
return {
|
|
948
|
-
status: "failed",
|
|
949
|
-
reason: String(error?.message || error)
|
|
950
|
-
};
|
|
951
|
-
}
|
|
1150
|
+
//#endregion
|
|
1151
|
+
//#region src/macos/instances.ts
|
|
1152
|
+
const BUNDLE_EXECUTABLE = /^[^/]+\.app\/Contents\/MacOS\/[^/]+$/;
|
|
1153
|
+
function bundleDirs(root) {
|
|
1154
|
+
const dir = macosDir(root);
|
|
1155
|
+
if (process.platform !== "darwin" || !existsSync(dir)) return [];
|
|
1156
|
+
return [.../* @__PURE__ */ new Set([dir, realpathSync(dir)])];
|
|
952
1157
|
}
|
|
953
|
-
function
|
|
954
|
-
|
|
955
|
-
return eraseParkedAfter("android", avdName, (_record, eraseStarted) => {
|
|
956
|
-
if (resolveOwnedAvdSerial(avdName, { timeoutMs: AVD_INVENTORY_TIMEOUT_MS }).serial) throw new Error(`AVD ${avdName} is running; it was kept`);
|
|
957
|
-
const result = teardownOwnedAvd(avdName, {
|
|
958
|
-
wipe: true,
|
|
959
|
-
owner: { pool: true },
|
|
960
|
-
onWipeStart: eraseStarted
|
|
961
|
-
});
|
|
962
|
-
if (result.status !== "torn-down") throw new Error(result.reason ?? `AVD ${avdName} is ${result.status}`);
|
|
963
|
-
}) ? {
|
|
964
|
-
status: "torn-down",
|
|
965
|
-
label: avdName
|
|
966
|
-
} : {
|
|
967
|
-
status: "skipped",
|
|
968
|
-
kind: "not-parked",
|
|
969
|
-
reason: "emulator is no longer parked"
|
|
970
|
-
};
|
|
971
|
-
} catch (error) {
|
|
972
|
-
return {
|
|
973
|
-
status: "failed",
|
|
974
|
-
reason: String(error?.message || error)
|
|
975
|
-
};
|
|
976
|
-
}
|
|
1158
|
+
function isBundleExecutable(path, dirs) {
|
|
1159
|
+
return dirs.some((dir) => path.startsWith(`${dir}/`) && BUNDLE_EXECUTABLE.test(path.slice(dir.length + 1)));
|
|
977
1160
|
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
const
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
try {
|
|
985
|
-
return root && realpathSync(root) !== realpathSync(workspace) ? root : void 0;
|
|
986
|
-
} catch {
|
|
987
|
-
return;
|
|
1161
|
+
/** Pids from `ps -axww -o pid=,comm=` output whose command names a bundle executable inside `dirs`. */
|
|
1162
|
+
function bundleCandidates(psOutput, dirs) {
|
|
1163
|
+
const pids = [];
|
|
1164
|
+
for (const line of psOutput.split("\n")) {
|
|
1165
|
+
const match = /^\s*(\d+)\s+(.+)$/.exec(line);
|
|
1166
|
+
if (match && isBundleExecutable(match[2], dirs)) pids.push(Number(match[1]));
|
|
988
1167
|
}
|
|
1168
|
+
return pids;
|
|
989
1169
|
}
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1170
|
+
/** Whether `lsof -Fn -d txt` output for one process names a bundle executable inside `dirs`. */
|
|
1171
|
+
function lsofNamesBundleExecutable(lsofOutput, dirs) {
|
|
1172
|
+
return lsofOutput.split("\n").some((line) => line.startsWith("n") && isBundleExecutable(line.slice(1), dirs));
|
|
1173
|
+
}
|
|
1174
|
+
function candidatePids(dirs) {
|
|
1175
|
+
if (!dirs.length) return [];
|
|
1176
|
+
return bundleCandidates(getExecutor().runFile("ps", [
|
|
1177
|
+
"-axww",
|
|
1178
|
+
"-o",
|
|
1179
|
+
"pid=,comm="
|
|
1180
|
+
], { timeoutMs: 1e4 }), dirs).filter((pid) => pid !== process.pid);
|
|
1181
|
+
}
|
|
1182
|
+
/** Pids that claim to run an app bundle from the workspace's macOS directory. `ps` reports argv[0], so this is a lead. */
|
|
1183
|
+
function bundleInstancePids(root) {
|
|
1184
|
+
return candidatePids(bundleDirs(root));
|
|
1185
|
+
}
|
|
1186
|
+
async function bundleInstancePidsAsync(root, signal, operation) {
|
|
1187
|
+
signal?.throwIfAborted();
|
|
1188
|
+
const dirs = bundleDirs(root);
|
|
1189
|
+
if (!dirs.length) return [];
|
|
1190
|
+
return bundleCandidates(await runOperationFile(operation, getExecutor(), "ps", [
|
|
1191
|
+
"-axww",
|
|
1192
|
+
"-o",
|
|
1193
|
+
"pid=,comm="
|
|
1194
|
+
], {
|
|
1195
|
+
timeoutMs: 1e4,
|
|
1196
|
+
killSignal: "SIGKILL",
|
|
1197
|
+
signal,
|
|
1198
|
+
cwd: operation?.cwd
|
|
1199
|
+
}), dirs).filter((pid) => pid !== process.pid);
|
|
1200
|
+
}
|
|
1201
|
+
function unverified(pid, why) {
|
|
1202
|
+
return Object.assign(/* @__PURE__ */ new Error(`Cannot verify macOS app pid ${pid}: ${why}; no signal was sent.`), { code: "STIM_MACOS_OWNER_UNVERIFIED" });
|
|
1203
|
+
}
|
|
1204
|
+
function bundleRecord(pid) {
|
|
1205
|
+
const processToken = (0, process_identity_exports.captureProcessToken)(pid);
|
|
1206
|
+
const start = (0, process_identity_exports.processStartMicros)(pid);
|
|
1207
|
+
if (!processToken || start.status !== "running") {
|
|
1208
|
+
if (start.status === "gone") return null;
|
|
1209
|
+
throw unverified(pid, "its identity cannot be captured");
|
|
1003
1210
|
}
|
|
1211
|
+
return {
|
|
1212
|
+
pid,
|
|
1213
|
+
processToken,
|
|
1214
|
+
startedAtMicros: start.startedAtMicros
|
|
1215
|
+
};
|
|
1004
1216
|
}
|
|
1005
|
-
function
|
|
1006
|
-
|
|
1217
|
+
function failedBundleListing(record, error) {
|
|
1218
|
+
if (["gone", "different"].includes((0, process_identity_exports.inspectProcessIdentity)(record))) return null;
|
|
1219
|
+
const open = String(error.stdout ?? "");
|
|
1220
|
+
if (!open.trim()) throw unverified(record.pid, `lsof failed (${error.message.split("\n")[0]})`);
|
|
1221
|
+
return open;
|
|
1007
1222
|
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
const
|
|
1012
|
-
if (
|
|
1013
|
-
const
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1223
|
+
/** Verifies owned bundle instances without blocking on process and executable probes. */
|
|
1224
|
+
async function runningBundleInstancesAsync(root, operation) {
|
|
1225
|
+
operation?.signal.throwIfAborted();
|
|
1226
|
+
const dirs = bundleDirs(root);
|
|
1227
|
+
if (!dirs.length) return [];
|
|
1228
|
+
const listing = await runOperationFile(operation, getExecutor(), "ps", [
|
|
1229
|
+
"-axww",
|
|
1230
|
+
"-o",
|
|
1231
|
+
"pid=,comm="
|
|
1232
|
+
], {
|
|
1233
|
+
cwd: operation?.cwd,
|
|
1234
|
+
timeoutMs: 1e4,
|
|
1235
|
+
killSignal: "SIGKILL"
|
|
1018
1236
|
});
|
|
1019
|
-
|
|
1020
|
-
const
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1237
|
+
const instances = [];
|
|
1238
|
+
for (const pid of bundleCandidates(listing, dirs).filter((candidate) => candidate !== process.pid)) {
|
|
1239
|
+
operation?.signal.throwIfAborted();
|
|
1240
|
+
const record = bundleRecord(pid);
|
|
1241
|
+
if (!record) continue;
|
|
1242
|
+
let open;
|
|
1243
|
+
try {
|
|
1244
|
+
open = await runOperationFile(operation, getExecutor(), "lsof", [
|
|
1245
|
+
"-a",
|
|
1246
|
+
"-p",
|
|
1247
|
+
String(pid),
|
|
1248
|
+
"-d",
|
|
1249
|
+
"txt",
|
|
1250
|
+
"-Fn"
|
|
1251
|
+
], {
|
|
1252
|
+
cwd: operation?.cwd,
|
|
1253
|
+
timeoutMs: 1e4,
|
|
1254
|
+
killSignal: "SIGKILL"
|
|
1255
|
+
});
|
|
1256
|
+
} catch (error) {
|
|
1257
|
+
if (error instanceof WorkspaceProcessCleanupError) throw error;
|
|
1258
|
+
operation?.signal.throwIfAborted();
|
|
1259
|
+
const partial = failedBundleListing(record, error);
|
|
1260
|
+
if (partial === null) continue;
|
|
1261
|
+
open = partial;
|
|
1262
|
+
}
|
|
1263
|
+
if (lsofNamesBundleExecutable(open, dirs)) instances.push(record);
|
|
1264
|
+
}
|
|
1265
|
+
return instances;
|
|
1266
|
+
}
|
|
1267
|
+
//#endregion
|
|
1268
|
+
//#region src/engine/tunnel.ts
|
|
1269
|
+
function tunnelArgv(provider, port, ngrokUrl, logFile, tailscaleBinary = "tailscale") {
|
|
1270
|
+
if (provider === "tailscale") return {
|
|
1271
|
+
bin: tailscaleBinary,
|
|
1272
|
+
args: [
|
|
1273
|
+
"serve",
|
|
1274
|
+
`--https=${port}`,
|
|
1275
|
+
`http://127.0.0.1:${port}`
|
|
1276
|
+
]
|
|
1277
|
+
};
|
|
1278
|
+
if (provider === "cloudflared") return {
|
|
1279
|
+
bin: "cloudflared",
|
|
1280
|
+
args: [
|
|
1281
|
+
"tunnel",
|
|
1282
|
+
"--url",
|
|
1283
|
+
`http://127.0.0.1:${port}`,
|
|
1284
|
+
...logFile ? ["--logfile", logFile] : []
|
|
1285
|
+
]
|
|
1029
1286
|
};
|
|
1030
1287
|
return {
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1288
|
+
bin: "ngrok",
|
|
1289
|
+
args: [
|
|
1290
|
+
"http",
|
|
1291
|
+
String(port),
|
|
1292
|
+
`--log=${logFile || "stdout"}`,
|
|
1293
|
+
"--log-format=json",
|
|
1294
|
+
...ngrokUrl ? ["--url", ngrokUrl] : []
|
|
1295
|
+
]
|
|
1039
1296
|
};
|
|
1040
1297
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1298
|
+
const CLOUDFLARED_URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/i;
|
|
1299
|
+
function parseCloudflaredLine(line) {
|
|
1300
|
+
const match = line.match(CLOUDFLARED_URL_RE);
|
|
1301
|
+
return match ? match[0] : null;
|
|
1302
|
+
}
|
|
1303
|
+
function parseNgrokLine(line) {
|
|
1304
|
+
let data;
|
|
1043
1305
|
try {
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
kind: "not-owned",
|
|
1048
|
-
reason: `sim "${resolved.notOwned}" is not Stim-owned: Stim has no record of creating it`
|
|
1049
|
-
};
|
|
1050
|
-
if (resolved.missing) return { status: "missing" };
|
|
1051
|
-
closeOwnedDeviceSessions({
|
|
1052
|
-
platform: "ios",
|
|
1053
|
-
id: udid
|
|
1054
|
-
}, () => Boolean(resolveOwnedIosSim(udid).sim), workspace);
|
|
1055
|
-
const current = resolveOwnedIosSim(udid);
|
|
1056
|
-
if (current.missing) return { status: "missing" };
|
|
1057
|
-
if (current.notOwned) return {
|
|
1058
|
-
status: "skipped",
|
|
1059
|
-
kind: "not-owned",
|
|
1060
|
-
reason: "simulator ownership changed before shutdown"
|
|
1061
|
-
};
|
|
1062
|
-
shutdownIosSim(udid);
|
|
1063
|
-
const sim = resolved.sim;
|
|
1064
|
-
if (del && park && park.max > 0) try {
|
|
1065
|
-
const { record, evicted } = parkOwnedIosSim(settleIosSimShutdown(udid, shutdownClock), udid, park);
|
|
1066
|
-
const removed = [];
|
|
1067
|
-
const failures = [];
|
|
1068
|
-
for (const entry of evicted) {
|
|
1069
|
-
const result = teardownParkedIosSim(entry.udid, { label: entry.name });
|
|
1070
|
-
if (result.status === "torn-down") removed.push({
|
|
1071
|
-
udid: entry.udid,
|
|
1072
|
-
name: entry.name
|
|
1073
|
-
});
|
|
1074
|
-
else if (result.status === "failed") failures.push(`could not delete evicted ${entry.name} (${entry.udid}): ${result.reason}`);
|
|
1075
|
-
}
|
|
1076
|
-
return {
|
|
1077
|
-
status: "torn-down",
|
|
1078
|
-
label: label ?? sim.name ?? udid,
|
|
1079
|
-
parked: {
|
|
1080
|
-
udid: record.udid,
|
|
1081
|
-
name: record.name
|
|
1082
|
-
},
|
|
1083
|
-
evicted: removed,
|
|
1084
|
-
...failures.length ? { evictionFailures: failures } : {}
|
|
1085
|
-
};
|
|
1086
|
-
} catch (e) {
|
|
1087
|
-
parkFallback = String(e?.message || e);
|
|
1088
|
-
}
|
|
1089
|
-
if (del) {
|
|
1090
|
-
deleteIosSim(udid);
|
|
1091
|
-
return {
|
|
1092
|
-
status: "torn-down",
|
|
1093
|
-
label: label ?? sim.name ?? udid,
|
|
1094
|
-
...parkFallback ? { parkFallback } : {}
|
|
1095
|
-
};
|
|
1096
|
-
}
|
|
1097
|
-
const settled = settleIosSimShutdown(udid, shutdownClock);
|
|
1098
|
-
if (settled.sim && settled.sim.state !== "Shutdown") return {
|
|
1099
|
-
status: "failed",
|
|
1100
|
-
label: label ?? sim.name ?? udid,
|
|
1101
|
-
reason: iosShutdownFailureReason(udid, settled.sim.state)
|
|
1102
|
-
};
|
|
1103
|
-
const root = sim.state === "Shutdown" ? void 0 : repoRootAbove(workspace);
|
|
1104
|
-
if (root && settled.sim) closeOwnedDeviceSessions({
|
|
1105
|
-
platform: "ios",
|
|
1106
|
-
id: udid
|
|
1107
|
-
}, () => resolveOwnedIosSim(udid).sim?.state === "Shutdown", workspace, root);
|
|
1108
|
-
return {
|
|
1109
|
-
status: "torn-down",
|
|
1110
|
-
label: label ?? sim.name ?? udid
|
|
1111
|
-
};
|
|
1112
|
-
} catch (e) {
|
|
1113
|
-
return {
|
|
1114
|
-
status: "failed",
|
|
1115
|
-
reason: String(e?.message || e),
|
|
1116
|
-
...parkFallback ? { parkFallback } : {}
|
|
1117
|
-
};
|
|
1306
|
+
data = JSON.parse(line);
|
|
1307
|
+
} catch {
|
|
1308
|
+
return null;
|
|
1118
1309
|
}
|
|
1310
|
+
if (typeof data !== "object" || data === null) return null;
|
|
1311
|
+
const url = data.url;
|
|
1312
|
+
return typeof url === "string" && /^https?:\/\//i.test(url) ? url : null;
|
|
1119
1313
|
}
|
|
1120
|
-
function
|
|
1121
|
-
|
|
1122
|
-
if (!config) throw new Error("Cannot verify AVD references without a Stim config; the data was kept.");
|
|
1123
|
-
if (owner && "projectPath" in owner && owner.expectedRecord && !isDeepStrictEqual(deviceSlotPlatforms(config.projects[owner.projectPath], owner.slot)?.android, owner.expectedRecord)) throw new Error(`AVD ${avdName} has a changed workspace record; its data was kept.`);
|
|
1124
|
-
for (const [projectPath, project] of Object.entries(config.projects)) for (const { slot, platforms } of projectDeviceSlots(project)) {
|
|
1125
|
-
if (platforms.android?.avdName?.toLowerCase() !== avdName.toLowerCase()) continue;
|
|
1126
|
-
if (!owner || !("projectPath" in owner) || owner.projectPath !== projectPath || (owner.slot ?? "default") !== slot || !platforms.android.owned) throw new Error(`AVD ${avdName} is referenced by ${projectPath} (${slot}); its data was kept.`);
|
|
1127
|
-
}
|
|
1128
|
-
if (readParked("android", { config }).some((entry) => entry.name.toLowerCase() === avdName.toLowerCase()) && !(owner && "pool" in owner)) throw new Error(`AVD ${avdName} is in the emulator pool; its data was kept.`);
|
|
1314
|
+
function parseTailscaleLine(line) {
|
|
1315
|
+
return /^https:\/\/[^\s/]+\.ts\.net(:\d+)?\/?$/.test(line) ? line : null;
|
|
1129
1316
|
}
|
|
1130
|
-
function
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
renameSync(candidate.directory, destination);
|
|
1143
|
-
} catch (error) {
|
|
1144
|
-
forgetCreatedDevice("android", name);
|
|
1145
|
-
throw error;
|
|
1146
|
-
}
|
|
1147
|
-
forgetCreatedDevice("android", candidate.name);
|
|
1148
|
-
return destination;
|
|
1317
|
+
function parserFor(provider) {
|
|
1318
|
+
if (provider === "tailscale") return parseTailscaleLine;
|
|
1319
|
+
return provider === "cloudflared" ? parseCloudflaredLine : parseNgrokLine;
|
|
1320
|
+
}
|
|
1321
|
+
const URL_TIMEOUT_MS = 15e3;
|
|
1322
|
+
const REACHABLE_TIMEOUT_MS = 24e4;
|
|
1323
|
+
const REACHABLE_POLL_MS = 2e3;
|
|
1324
|
+
const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1325
|
+
async function withManagedTunnelLock(root, fn, options = {}) {
|
|
1326
|
+
return withWorkspaceProcessLock(managedTunnelLockRoot(root), "metro-tunnel", fn, {
|
|
1327
|
+
...options,
|
|
1328
|
+
rejectOwnerPurposes: ["workspace removal"]
|
|
1149
1329
|
});
|
|
1150
|
-
const current = lstatSync(detached);
|
|
1151
|
-
if (!current.isDirectory() || current.dev !== candidate.dev || current.ino !== candidate.ino) throw new Error(`Detached AVD data changed at ${detached}; it was kept.`);
|
|
1152
|
-
try {
|
|
1153
|
-
rmSync(detached, { recursive: true });
|
|
1154
|
-
} catch (error) {
|
|
1155
|
-
throw new Error(`Could not remove AVD data at ${detached}; retry gc --delete: ${error.message}`, { cause: error });
|
|
1156
|
-
}
|
|
1157
|
-
forgetCreatedDevice("android", basename(detached, ".avd"));
|
|
1158
1330
|
}
|
|
1159
|
-
function
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
}
|
|
1165
|
-
if (!del) return {
|
|
1166
|
-
status: "skipped",
|
|
1167
|
-
reason: "AVD registration is missing; its data was kept."
|
|
1168
|
-
};
|
|
1169
|
-
for (const candidate of candidates) {
|
|
1170
|
-
if (candidate.name !== avdName) throw new Error(`AVD data does not match ${avdName}; it was kept.`);
|
|
1171
|
-
removeOrphanedAvdDirectory(candidate, owner);
|
|
1172
|
-
}
|
|
1173
|
-
return {
|
|
1174
|
-
status: "torn-down",
|
|
1175
|
-
label: avdName
|
|
1176
|
-
};
|
|
1331
|
+
async function withManagedTunnelRemovalLock(root, fn, options = {}) {
|
|
1332
|
+
return withWorkspaceProcessLock(managedTunnelLockRoot(root), "metro-tunnel", fn, {
|
|
1333
|
+
...options,
|
|
1334
|
+
ownerPurpose: "workspace removal"
|
|
1335
|
+
});
|
|
1177
1336
|
}
|
|
1178
|
-
function
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1337
|
+
async function withManagedRemoteWorktreeLock(worktreeRoot, fn, options = {}) {
|
|
1338
|
+
return withWorkspaceProcessLock(managedRemoteWorktreeLockRoot(worktreeRoot), "managed-remote", fn, {
|
|
1339
|
+
...options,
|
|
1340
|
+
external: true,
|
|
1341
|
+
rejectOwnerPurposes: ["worktree removal"]
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
async function withManagedRemoteWorktreeRemovalLock(worktreeRoot, fn, options = {}) {
|
|
1345
|
+
return withWorkspaceProcessLock(managedRemoteWorktreeLockRoot(worktreeRoot), "managed-remote", fn, {
|
|
1346
|
+
...options,
|
|
1347
|
+
external: true,
|
|
1348
|
+
ownerPurpose: "worktree removal"
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
function managedLockHolders(root) {
|
|
1352
|
+
const locks = [{
|
|
1353
|
+
label: "managed tunnel",
|
|
1354
|
+
path: workspaceProcessLockPath(managedTunnelLockRoot(root), "metro-tunnel", false)
|
|
1355
|
+
}, {
|
|
1356
|
+
label: "managed remote",
|
|
1357
|
+
path: workspaceProcessLockPath(managedRemoteWorktreeLockRoot(root), "managed-remote", true)
|
|
1358
|
+
}];
|
|
1359
|
+
const reasons = [];
|
|
1360
|
+
for (const { label, path } of locks) {
|
|
1361
|
+
const claims = (0, ownership_claim_exports.readClaimSet)(path);
|
|
1362
|
+
const holder = claims.live[0];
|
|
1363
|
+
if (holder) {
|
|
1364
|
+
const purpose = typeof holder.details.purpose === "string" ? ` for ${holder.details.purpose}` : "";
|
|
1365
|
+
reasons.push(`the ${label} lock is held${purpose}`);
|
|
1366
|
+
} else if (claims.unresolved[0]) reasons.push(`the ${label} lock cannot be resolved: ${claims.unresolved[0].reason}`);
|
|
1197
1367
|
}
|
|
1368
|
+
return reasons;
|
|
1198
1369
|
}
|
|
1199
|
-
function
|
|
1200
|
-
const
|
|
1201
|
-
return
|
|
1202
|
-
...adb.emulators,
|
|
1203
|
-
...adb.physical,
|
|
1204
|
-
...adb.unhealthy
|
|
1205
|
-
].some((entry) => entry.serial === serial);
|
|
1370
|
+
function managedTunnelLockRoot(root) {
|
|
1371
|
+
const key = createHash("sha256").update(resolve(root)).digest("hex");
|
|
1372
|
+
return join(getConfigDir(), "process-locks", key);
|
|
1206
1373
|
}
|
|
1207
|
-
function
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
};
|
|
1239
|
-
closeOwnedDeviceSessions({
|
|
1240
|
-
platform: "android",
|
|
1241
|
-
id: serial,
|
|
1242
|
-
avdName
|
|
1243
|
-
}, stillOwned, workspace);
|
|
1244
|
-
if (!stillOwned()) throw new Error(`Owned AVD ${avdName} changed before shutdown; it was kept.`);
|
|
1245
|
-
waitForShutdown(avdName, (timeoutMs) => {
|
|
1246
|
-
(0, ownership_claim_exports.markClaimChildPending)(claim);
|
|
1247
|
-
try {
|
|
1248
|
-
shutdownAndroidEmulator(serial, timeoutMs);
|
|
1249
|
-
} finally {
|
|
1250
|
-
(0, ownership_claim_exports.clearClaimChild)(claim);
|
|
1251
|
-
}
|
|
1252
|
-
});
|
|
1253
|
-
const root = repoRootAbove(workspace);
|
|
1254
|
-
if (root) {
|
|
1255
|
-
const deadline = Date.now() + ADB_RELEASE_WAIT_MS;
|
|
1256
|
-
while (!serialFree(serial, Math.max(1, deadline - Date.now())) && Date.now() < deadline) sleepSync(250);
|
|
1257
|
-
closeOwnedDeviceSessions({
|
|
1258
|
-
platform: "android",
|
|
1259
|
-
id: serial,
|
|
1260
|
-
avdName
|
|
1261
|
-
}, stillStopped, workspace, root);
|
|
1262
|
-
}
|
|
1263
|
-
} else {
|
|
1264
|
-
closeOwnedDeviceSessions({
|
|
1265
|
-
platform: "android",
|
|
1266
|
-
id: null,
|
|
1267
|
-
avdName
|
|
1268
|
-
}, stillStopped, workspace);
|
|
1269
|
-
waitForShutdown(avdName, null);
|
|
1270
|
-
}
|
|
1271
|
-
if (wipe && !del) {
|
|
1272
|
-
const current = resolveAvd(avdName);
|
|
1273
|
-
if (current.notOwned || current.missing) throw new Error(`AVD ${avdName} changed before its data was wiped.`);
|
|
1274
|
-
if (current.serial) throw new Error(`Owned AVD ${avdName} started again before its data was wiped.`);
|
|
1275
|
-
assertStopped(avdName);
|
|
1276
|
-
withConfigLock$1(() => assertAvdReferences(avdName, owner));
|
|
1277
|
-
const directory = ownedAvdDirectory(avdName);
|
|
1278
|
-
if (!directory) throw new Error(`The data directory of AVD ${avdName} could not be resolved.`);
|
|
1279
|
-
onWipeStart?.();
|
|
1280
|
-
wipeAvdUserData(directory);
|
|
1281
|
-
}
|
|
1282
|
-
if (del) {
|
|
1283
|
-
const current = resolveAvd(avdName);
|
|
1284
|
-
if (current.notOwned) return {
|
|
1285
|
-
status: "skipped",
|
|
1286
|
-
kind: "not-owned",
|
|
1287
|
-
reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
|
|
1288
|
-
};
|
|
1289
|
-
if (current.missing) return teardownUnregisteredAvd(avdName, del, owner);
|
|
1290
|
-
if (current.serial) throw new Error(`Owned AVD ${avdName} started again before deletion.`);
|
|
1291
|
-
assertStopped(avdName);
|
|
1292
|
-
withConfigLock$1(() => assertAvdReferences(avdName, owner));
|
|
1293
|
-
if (park && park.max > 0) try {
|
|
1294
|
-
const systemImage = ownedAvdSystemImage(avdName);
|
|
1295
|
-
if (!systemImage || !park.configuration || !ownedAvdMatchesConfiguration(avdName, park.configuration)) throw new Error("the AVD has no verified creation configuration");
|
|
1296
|
-
const evicted = parkSim({
|
|
1297
|
-
platform: "android",
|
|
1298
|
-
projectPath: park.projectPath,
|
|
1299
|
-
slot: park.slot,
|
|
1300
|
-
max: park.max,
|
|
1301
|
-
record: {
|
|
1302
|
-
udid: avdName,
|
|
1303
|
-
name: avdName,
|
|
1304
|
-
systemImage,
|
|
1305
|
-
configuration: park.configuration,
|
|
1306
|
-
parkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1307
|
-
...park.appId ? { packageName: park.appId } : {},
|
|
1308
|
-
...park.cacheKey ? { cacheKey: park.cacheKey } : {}
|
|
1309
|
-
}
|
|
1310
|
-
});
|
|
1311
|
-
const removed = [];
|
|
1312
|
-
const failures = [];
|
|
1313
|
-
for (const entry of evicted) {
|
|
1314
|
-
const result = teardownParkedAvd(entry.name);
|
|
1315
|
-
if (result.status === "torn-down") removed.push({
|
|
1316
|
-
udid: entry.name,
|
|
1317
|
-
name: entry.name,
|
|
1318
|
-
platform: "android"
|
|
1319
|
-
});
|
|
1320
|
-
else if (result.status === "failed") failures.push(`could not delete evicted ${entry.name}: ${result.reason}`);
|
|
1321
|
-
}
|
|
1322
|
-
return {
|
|
1323
|
-
status: "torn-down",
|
|
1324
|
-
label: avdName,
|
|
1325
|
-
parked: {
|
|
1326
|
-
udid: avdName,
|
|
1327
|
-
name: avdName,
|
|
1328
|
-
platform: "android"
|
|
1329
|
-
},
|
|
1330
|
-
evicted: removed,
|
|
1331
|
-
...failures.length ? { evictionFailures: failures } : {}
|
|
1332
|
-
};
|
|
1333
|
-
} catch (error) {
|
|
1334
|
-
parkFallback = String(error?.message || error);
|
|
1335
|
-
}
|
|
1336
|
-
const directory = ownedAvdDirectory(avdName);
|
|
1337
|
-
const paths = avdStorageRoots().map((root) => join(root, `${avdName}.ini`));
|
|
1338
|
-
if (directory) paths.push(directory);
|
|
1339
|
-
(0, ownership_claim_exports.markClaimChildPending)(claim);
|
|
1340
|
-
try {
|
|
1341
|
-
deleteAvd(avdName);
|
|
1342
|
-
} finally {
|
|
1343
|
-
(0, ownership_claim_exports.clearClaimChild)(claim);
|
|
1344
|
-
}
|
|
1345
|
-
const remaining = paths.filter(avdPathExists);
|
|
1346
|
-
if (remaining.length) throw new Error(`AVD ${avdName} was only partially deleted; data remains at ${remaining.join(", ")}.`);
|
|
1347
|
-
}
|
|
1348
|
-
return {
|
|
1349
|
-
status: "torn-down",
|
|
1350
|
-
label: avdName,
|
|
1351
|
-
serial: resolved.serial ?? null,
|
|
1352
|
-
...parkFallback ? { parkFallback } : {}
|
|
1374
|
+
function managedRemoteWorktreeLockRoot(worktreeRoot) {
|
|
1375
|
+
const key = createHash("sha256").update(resolve(worktreeRoot)).digest("hex");
|
|
1376
|
+
return join(getConfigDir(), "process-locks", "worktrees", key);
|
|
1377
|
+
}
|
|
1378
|
+
async function defaultProbeReachable(url, signal) {
|
|
1379
|
+
const status = await probePublicHttp(url, signal);
|
|
1380
|
+
return status !== null && status > 0;
|
|
1381
|
+
}
|
|
1382
|
+
function waitForUrl(child, parseLine, timeoutMs, signal) {
|
|
1383
|
+
return new Promise((resolve) => {
|
|
1384
|
+
let settled = false;
|
|
1385
|
+
let lastLine = null;
|
|
1386
|
+
let timer;
|
|
1387
|
+
let onOut;
|
|
1388
|
+
let onErr;
|
|
1389
|
+
let onError;
|
|
1390
|
+
let onExit;
|
|
1391
|
+
const finish = (url, exited = false) => {
|
|
1392
|
+
if (settled) return;
|
|
1393
|
+
settled = true;
|
|
1394
|
+
clearTimeout(timer);
|
|
1395
|
+
child.stdout?.removeListener("data", onOut);
|
|
1396
|
+
child.stderr?.removeListener("data", onErr);
|
|
1397
|
+
child.removeListener("error", onError);
|
|
1398
|
+
child.removeListener("exit", onExit);
|
|
1399
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1400
|
+
resolve({
|
|
1401
|
+
url,
|
|
1402
|
+
exited,
|
|
1403
|
+
lastLine
|
|
1404
|
+
});
|
|
1353
1405
|
};
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1406
|
+
const onAbort = () => finish(null);
|
|
1407
|
+
const onLine = (line) => {
|
|
1408
|
+
if (settled) return;
|
|
1409
|
+
if (line.trim()) lastLine = line.trim();
|
|
1410
|
+
const url = parseLine(line);
|
|
1411
|
+
if (url) finish(url);
|
|
1358
1412
|
};
|
|
1359
|
-
|
|
1413
|
+
const outReader = createLineReader(onLine);
|
|
1414
|
+
const errReader = createLineReader(onLine);
|
|
1415
|
+
onOut = (chunk) => outReader.push(chunk);
|
|
1416
|
+
onErr = (chunk) => errReader.push(chunk);
|
|
1417
|
+
onError = () => finish(null);
|
|
1418
|
+
onExit = () => {
|
|
1419
|
+
outReader.flush();
|
|
1420
|
+
errReader.flush();
|
|
1421
|
+
finish(null, true);
|
|
1422
|
+
};
|
|
1423
|
+
child.stdout?.setEncoding?.("utf-8");
|
|
1424
|
+
child.stderr?.setEncoding?.("utf-8");
|
|
1425
|
+
child.stdout?.on("data", onOut);
|
|
1426
|
+
child.stderr?.on("data", onErr);
|
|
1427
|
+
child.on("error", onError);
|
|
1428
|
+
child.on("exit", onExit);
|
|
1429
|
+
timer = setTimeout(() => finish(null), timeoutMs);
|
|
1430
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1431
|
+
if (signal?.aborted) onAbort();
|
|
1432
|
+
});
|
|
1360
1433
|
}
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
const
|
|
1366
|
-
|
|
1367
|
-
|
|
1434
|
+
function waitForFileUrl(child, logFile, parseLine, timeoutMs, signal) {
|
|
1435
|
+
return new Promise((resolve) => {
|
|
1436
|
+
let settled = false;
|
|
1437
|
+
let watcher = null;
|
|
1438
|
+
const finish = (url, exited = false) => {
|
|
1439
|
+
if (settled) return;
|
|
1440
|
+
settled = true;
|
|
1441
|
+
watcher?.close();
|
|
1442
|
+
clearInterval(interval);
|
|
1443
|
+
clearTimeout(timer);
|
|
1444
|
+
child.removeListener("error", onError);
|
|
1445
|
+
child.removeListener("exit", onExit);
|
|
1446
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1447
|
+
resolve({
|
|
1448
|
+
url,
|
|
1449
|
+
exited
|
|
1450
|
+
});
|
|
1451
|
+
};
|
|
1452
|
+
const onAbort = () => finish(null);
|
|
1453
|
+
const probe = () => {
|
|
1454
|
+
try {
|
|
1455
|
+
for (const line of readFileSync(logFile, "utf-8").split(/\r?\n/)) {
|
|
1456
|
+
const url = parseLine(line);
|
|
1457
|
+
if (url) return finish(url);
|
|
1458
|
+
}
|
|
1459
|
+
} catch {}
|
|
1460
|
+
};
|
|
1461
|
+
const onError = () => finish(null);
|
|
1462
|
+
const onExit = () => finish(null, true);
|
|
1463
|
+
child.on("error", onError);
|
|
1464
|
+
child.on("exit", onExit);
|
|
1368
1465
|
try {
|
|
1369
|
-
|
|
1466
|
+
watcher = watch(dirname(logFile), (_event, name) => {
|
|
1467
|
+
if (name === null || name.toString() === basename(logFile)) probe();
|
|
1468
|
+
});
|
|
1469
|
+
watcher.on("error", () => {
|
|
1470
|
+
watcher?.close();
|
|
1471
|
+
watcher = null;
|
|
1472
|
+
});
|
|
1473
|
+
watcher.unref();
|
|
1370
1474
|
} catch {}
|
|
1371
|
-
const
|
|
1372
|
-
|
|
1475
|
+
const interval = setInterval(probe, Math.min(1e3, timeoutMs));
|
|
1476
|
+
const timer = setTimeout(() => finish(null), timeoutMs);
|
|
1477
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1478
|
+
if (signal?.aborted) onAbort();
|
|
1479
|
+
else probe();
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
function createTunnelLogFile(provider) {
|
|
1483
|
+
const dir = join(getConfigDir(), "tunnel-logs");
|
|
1484
|
+
mkdirSync(dir, { recursive: true });
|
|
1485
|
+
const path = join(dir, `${provider}-${randomUUID()}.log`);
|
|
1486
|
+
closeSync(openSync(path, "wx", 384));
|
|
1487
|
+
return path;
|
|
1488
|
+
}
|
|
1489
|
+
function removeTunnelLogFile(path) {
|
|
1490
|
+
if (!path) return;
|
|
1491
|
+
try {
|
|
1492
|
+
unlinkSync(path);
|
|
1493
|
+
} catch {}
|
|
1494
|
+
}
|
|
1495
|
+
function removeRecordedTunnelLogFile(path) {
|
|
1496
|
+
if (!path) return;
|
|
1497
|
+
const dir = resolve(join(getConfigDir(), "tunnel-logs"));
|
|
1498
|
+
const resolved = resolve(path);
|
|
1499
|
+
if (!resolved.startsWith(`${dir}${sep}`) || resolve(join(dir, basename(resolved))) !== resolved) return;
|
|
1500
|
+
removeTunnelLogFile(resolved);
|
|
1501
|
+
}
|
|
1502
|
+
async function waitUntilReachable({ url, now, sleep, probe, timeoutMs, signal }) {
|
|
1503
|
+
const deadline = now() + timeoutMs;
|
|
1504
|
+
for (;;) {
|
|
1505
|
+
throwIfCancelled(signal, "Metro tunnel startup");
|
|
1506
|
+
const controller = new AbortController();
|
|
1507
|
+
const ok = await probe(url, signal ? AbortSignal.any([signal, controller.signal]) : controller.signal).catch(() => false);
|
|
1508
|
+
throwIfCancelled(signal, "Metro tunnel startup");
|
|
1509
|
+
if (ok) return true;
|
|
1510
|
+
if (now() >= deadline) return false;
|
|
1511
|
+
if (sleep === defaultSleep) await cancellableSleep(REACHABLE_POLL_MS, {
|
|
1512
|
+
signal,
|
|
1513
|
+
what: "Metro tunnel startup"
|
|
1514
|
+
});
|
|
1515
|
+
else await sleep(REACHABLE_POLL_MS);
|
|
1373
1516
|
}
|
|
1374
|
-
return chromeProcessState(record, profile) === "gone";
|
|
1375
1517
|
}
|
|
1376
|
-
const
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1518
|
+
const CLEANUP_TIMEOUT_MS = 1e3;
|
|
1519
|
+
async function startTunnel({ claims = [], signal, provider, port, spawnFn = null, urlTimeoutMs = URL_TIMEOUT_MS, reachableTimeoutMs = REACHABLE_TIMEOUT_MS, probeReachable = defaultProbeReachable, now = Date.now, sleep = defaultSleep, ngrokUrl = null, requireReachable = true, cleanupTimeoutMs = CLEANUP_TIMEOUT_MS, isChildAlive = pidExists, readProcessToken = process_identity_exports.captureProcessToken, logFile = null }) {
|
|
1520
|
+
throwIfCancelled(signal, "Metro tunnel startup");
|
|
1521
|
+
const spawn = spawnFn || ((cmd, args, opts) => getExecutor().spawn(cmd, args, opts));
|
|
1522
|
+
const outputFile = provider === "tailscale" ? null : logFile || (spawnFn ? null : createTunnelLogFile(provider));
|
|
1523
|
+
let child;
|
|
1382
1524
|
try {
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1525
|
+
const binary = provider === "tailscale" ? resolveTailscaleBinary((bin) => getExecutor().findExecutable(bin)) : provider;
|
|
1526
|
+
if (!binary) throw new Error("Install Tailscale and sign in to your tailnet.");
|
|
1527
|
+
const { bin, args } = tunnelArgv(provider, port, ngrokUrl, outputFile, binary);
|
|
1528
|
+
for (const claim of claims) (0, ownership_claim_exports.markClaimChildPending)(claim);
|
|
1529
|
+
child = spawn(bin, args, {
|
|
1530
|
+
stdio: outputFile ? [
|
|
1531
|
+
"ignore",
|
|
1532
|
+
"ignore",
|
|
1533
|
+
"ignore"
|
|
1534
|
+
] : [
|
|
1535
|
+
"ignore",
|
|
1536
|
+
"pipe",
|
|
1537
|
+
"pipe"
|
|
1538
|
+
],
|
|
1539
|
+
detached: true
|
|
1386
1540
|
});
|
|
1387
|
-
} catch (
|
|
1541
|
+
} catch (err) {
|
|
1542
|
+
for (const claim of claims) (0, ownership_claim_exports.clearClaimChild)(claim);
|
|
1543
|
+
removeTunnelLogFile(outputFile);
|
|
1388
1544
|
return {
|
|
1389
|
-
|
|
1390
|
-
reason:
|
|
1545
|
+
failed: true,
|
|
1546
|
+
reason: `Could not start ${provider}: ${describe(err)}`
|
|
1391
1547
|
};
|
|
1392
1548
|
}
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
if (
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
if (!await (0, process_identity_exports.waitForProcessExit)(record, BROWSER_EXIT_WAIT_MS)) return {
|
|
1412
|
-
status: "failed",
|
|
1413
|
-
reason: `browser supervisor pid ${record.pid} did not exit`
|
|
1414
|
-
};
|
|
1415
|
-
}
|
|
1416
|
-
if (record.chromeProcess && !await stopOwnedChrome(record.chromeProcess, profile)) return {
|
|
1417
|
-
status: "failed",
|
|
1418
|
-
reason: `Chrome pid ${record.chromeProcess.pid} did not exit`
|
|
1419
|
-
};
|
|
1420
|
-
const exited = record.chromeProcess;
|
|
1421
|
-
const holder = liveProfileHolder(profile, { alive: (pid) => pidExists(pid) && !(pid === exited?.pid && (0, process_identity_exports.inspectProcessIdentity)(exited) === "gone") });
|
|
1422
|
-
if (holder !== null) return {
|
|
1423
|
-
status: "skipped",
|
|
1424
|
-
kind: "not-verified",
|
|
1425
|
-
reason: `pid ${holder} still holds ${profile}, and the browser record does not name it; it was left running`
|
|
1426
|
-
};
|
|
1427
|
-
const claims = (0, ownership_claim_exports.clearFreeClaimSet)({
|
|
1428
|
-
root: webClaimRoot(root),
|
|
1429
|
-
label: "browser supervisor"
|
|
1430
|
-
});
|
|
1431
|
-
if (claims.status !== "cleared") return {
|
|
1432
|
-
status: "skipped",
|
|
1433
|
-
kind: "not-verified",
|
|
1434
|
-
reason: claims.status === "held" ? `the browser supervisor claim is still held${claims.holder ? ` by pid ${claims.holder.owner.pid}` : ""}` : `the browser supervisor claim could not be cleared (${claims.reason}); remove it with ${(0, ownership_claim_exports.claimRemoveCommand)(webClaimRoot(root))}`
|
|
1435
|
-
};
|
|
1436
|
-
clearWebRecord(root, record);
|
|
1437
|
-
await releaseBrowserPort(root, record.cdpPort);
|
|
1438
|
-
} else {
|
|
1439
|
-
const holder = liveProfileHolder(profile);
|
|
1440
|
-
if (holder !== null) return {
|
|
1441
|
-
status: "skipped",
|
|
1442
|
-
kind: "not-verified",
|
|
1443
|
-
reason: `pid ${holder} holds ${profile} but no browser supervisor is recorded; it was left running`
|
|
1444
|
-
};
|
|
1445
|
-
const port = getProject(root)?.ports?.[CDP_PORT_LABEL];
|
|
1446
|
-
if (typeof port === "number") await releaseBrowserPort(root, port);
|
|
1549
|
+
let childExited = false;
|
|
1550
|
+
let childIdentity = null;
|
|
1551
|
+
child.once("exit", () => {
|
|
1552
|
+
childExited = true;
|
|
1553
|
+
});
|
|
1554
|
+
const cleanup = async (alreadyExited = childExited) => {
|
|
1555
|
+
if (claims.length > 0 && childIdentity && process.platform !== "win32" && (0, process_identity_exports.inspectProcessIdentity)(childIdentity) === "same") try {
|
|
1556
|
+
signalProcessTree(childIdentity.pid, "SIGTERM", { group: true });
|
|
1557
|
+
} catch {}
|
|
1558
|
+
const drained = await terminateChild(child, {
|
|
1559
|
+
alreadyExited,
|
|
1560
|
+
timeoutMs: cleanupTimeoutMs,
|
|
1561
|
+
isAlive: isChildAlive,
|
|
1562
|
+
waitForGroup: claims.length > 0 && process.platform !== "win32"
|
|
1563
|
+
}) && (claims.length === 0 || process.platform === "win32" || !child.pid || !(0, ownership_claim_exports.processGroupAlive)(child.pid));
|
|
1564
|
+
if (drained) {
|
|
1565
|
+
for (const claim of claims) (0, ownership_claim_exports.clearClaimChild)(claim);
|
|
1566
|
+
removeTunnelLogFile(outputFile);
|
|
1447
1567
|
}
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1568
|
+
return drained ? { status: "stopped" } : {
|
|
1569
|
+
status: "failed",
|
|
1570
|
+
reason: `Sent SIGKILL but could not confirm that pid ${child.pid ?? "unknown"} exited.`
|
|
1571
|
+
};
|
|
1572
|
+
};
|
|
1573
|
+
const failAfterCleanup = async (reason, alreadyExited = childExited) => {
|
|
1574
|
+
const stopped = await cleanup(alreadyExited);
|
|
1575
|
+
if (stopped.status === "failed") return {
|
|
1576
|
+
failed: true,
|
|
1577
|
+
reason: `${reason} ${stopped.reason}`,
|
|
1578
|
+
cleanupFailed: true
|
|
1579
|
+
};
|
|
1580
|
+
return {
|
|
1581
|
+
failed: true,
|
|
1582
|
+
reason: signal?.aborted ? "Metro tunnel startup was cancelled." : reason
|
|
1583
|
+
};
|
|
1584
|
+
};
|
|
1585
|
+
const pid = child.pid;
|
|
1586
|
+
if (!pid) return failAfterCleanup(`${provider} started but reported no pid.`);
|
|
1587
|
+
const captureChildIdentity = () => {
|
|
1588
|
+
try {
|
|
1589
|
+
return readProcessToken(pid);
|
|
1590
|
+
} catch {
|
|
1591
|
+
return null;
|
|
1460
1592
|
}
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1593
|
+
};
|
|
1594
|
+
const initialProcessToken = captureChildIdentity();
|
|
1595
|
+
if (!initialProcessToken) return failAfterCleanup(`${provider} started but its process identity token could not be read.`);
|
|
1596
|
+
childIdentity = {
|
|
1597
|
+
pid,
|
|
1598
|
+
processToken: initialProcessToken
|
|
1599
|
+
};
|
|
1600
|
+
if (claims.length > 0) try {
|
|
1601
|
+
for (const claim of claims) (0, ownership_claim_exports.setClaimChild)(claim, childIdentity);
|
|
1465
1602
|
} catch (error) {
|
|
1466
|
-
return {
|
|
1467
|
-
|
|
1468
|
-
|
|
1603
|
+
return failAfterCleanup(`Could not record ${provider} process ownership: ${describe(error)}`);
|
|
1604
|
+
}
|
|
1605
|
+
const { url, exited, lastLine } = outputFile ? await waitForFileUrl(child, outputFile, parserFor(provider), urlTimeoutMs, signal) : await waitForUrl(child, parserFor(provider), urlTimeoutMs, signal);
|
|
1606
|
+
if (!url) return failAfterCleanup(exited ? provider === "tailscale" && lastLine ? `${provider} exited before printing a tunnel URL: ${lastLine}` : `${provider} exited before printing a tunnel URL.` : `${provider} did not print a tunnel URL within ${urlTimeoutMs}ms.`, exited || childExited);
|
|
1607
|
+
if (!outputFile) resumeChildPipes(child);
|
|
1608
|
+
if (requireReachable) {
|
|
1609
|
+
let reachable;
|
|
1610
|
+
try {
|
|
1611
|
+
reachable = await waitUntilReachable({
|
|
1612
|
+
url,
|
|
1613
|
+
now,
|
|
1614
|
+
sleep,
|
|
1615
|
+
probe: probeReachable,
|
|
1616
|
+
timeoutMs: reachableTimeoutMs,
|
|
1617
|
+
signal
|
|
1618
|
+
});
|
|
1619
|
+
} catch (cause) {
|
|
1620
|
+
return failAfterCleanup(`Could not establish ${provider} tunnel reachability: ${describe(cause)}`);
|
|
1621
|
+
}
|
|
1622
|
+
if (!reachable) return failAfterCleanup(`${url} did not become reachable within ${reachableTimeoutMs}ms; ${provider} may still be registering, or the network path is blocked.`);
|
|
1623
|
+
}
|
|
1624
|
+
if (signal?.aborted) return failAfterCleanup("Metro tunnel startup was cancelled.");
|
|
1625
|
+
const finalProcessToken = captureChildIdentity();
|
|
1626
|
+
if (!finalProcessToken || finalProcessToken !== initialProcessToken) return failAfterCleanup(`${provider} process identity changed before its tunnel could be recorded.`);
|
|
1627
|
+
if (!outputFile) unrefChildPipes(child);
|
|
1628
|
+
child.unref?.();
|
|
1629
|
+
if (childExited) return failAfterCleanup(`${provider} exited before its tunnel could be recorded.`, true);
|
|
1630
|
+
return {
|
|
1631
|
+
url,
|
|
1632
|
+
pid,
|
|
1633
|
+
processToken: initialProcessToken,
|
|
1634
|
+
logFile: outputFile,
|
|
1635
|
+
cleanup: () => cleanup()
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
async function startTunnelSequence({ claims = [], signal, providers, port, ngrokUrl = null, requireReachable = true, start = startTunnel }) {
|
|
1639
|
+
const failures = [];
|
|
1640
|
+
for (const provider of providers) {
|
|
1641
|
+
if (signal?.aborted) return {
|
|
1642
|
+
failed: true,
|
|
1643
|
+
reason: "Metro tunnel startup was cancelled."
|
|
1644
|
+
};
|
|
1645
|
+
const result = await start({
|
|
1646
|
+
claims,
|
|
1647
|
+
signal,
|
|
1648
|
+
provider,
|
|
1649
|
+
port,
|
|
1650
|
+
ngrokUrl: provider === "ngrok" ? ngrokUrl : null,
|
|
1651
|
+
requireReachable
|
|
1652
|
+
});
|
|
1653
|
+
if (!("failed" in result)) return {
|
|
1654
|
+
provider,
|
|
1655
|
+
...result
|
|
1656
|
+
};
|
|
1657
|
+
failures.push(`${provider}: ${result.reason}`);
|
|
1658
|
+
if (result.cleanupFailed) return {
|
|
1659
|
+
failed: true,
|
|
1660
|
+
reason: failures.join(" "),
|
|
1661
|
+
cleanupFailed: true
|
|
1469
1662
|
};
|
|
1470
1663
|
}
|
|
1664
|
+
return {
|
|
1665
|
+
failed: true,
|
|
1666
|
+
reason: failures.join(" ") || "No managed tunnel provider was selected."
|
|
1667
|
+
};
|
|
1471
1668
|
}
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
function bundleDirs(root) {
|
|
1476
|
-
const dir = macosDir(root);
|
|
1477
|
-
if (process.platform !== "darwin" || !existsSync(dir)) return [];
|
|
1478
|
-
return [.../* @__PURE__ */ new Set([dir, realpathSync(dir)])];
|
|
1669
|
+
function closeChildPipes(child) {
|
|
1670
|
+
child.stdout?.destroy?.();
|
|
1671
|
+
child.stderr?.destroy?.();
|
|
1479
1672
|
}
|
|
1480
|
-
function
|
|
1481
|
-
|
|
1673
|
+
function unrefChildPipes(child) {
|
|
1674
|
+
resumeChildPipes(child);
|
|
1675
|
+
for (const stream of [child.stdout, child.stderr]) stream?.unref?.();
|
|
1482
1676
|
}
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
for (const line of psOutput.split("\n")) {
|
|
1487
|
-
const match = /^\s*(\d+)\s+(.+)$/.exec(line);
|
|
1488
|
-
if (match && isBundleExecutable(match[2], dirs)) pids.push(Number(match[1]));
|
|
1489
|
-
}
|
|
1490
|
-
return pids;
|
|
1677
|
+
function resumeChildPipes(child) {
|
|
1678
|
+
child.stdout?.resume?.();
|
|
1679
|
+
child.stderr?.resume?.();
|
|
1491
1680
|
}
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1681
|
+
function signalAndWaitForExit(child, signal, { timeoutMs, isAlive, platform }) {
|
|
1682
|
+
return new Promise((resolve) => {
|
|
1683
|
+
let settled = false;
|
|
1684
|
+
let timer;
|
|
1685
|
+
const onExit = () => finish(true);
|
|
1686
|
+
const onError = (error) => finish(isEsrch(error));
|
|
1687
|
+
const finish = (exited) => {
|
|
1688
|
+
if (settled) return;
|
|
1689
|
+
settled = true;
|
|
1690
|
+
clearTimeout(timer);
|
|
1691
|
+
child.removeListener("exit", onExit);
|
|
1692
|
+
child.removeListener("error", onError);
|
|
1693
|
+
resolve(exited);
|
|
1694
|
+
};
|
|
1695
|
+
child.once("exit", onExit);
|
|
1696
|
+
child.once("error", onError);
|
|
1697
|
+
try {
|
|
1698
|
+
if (platform === "win32" && child.pid) signalProcessTree(child.pid, signal, { platform });
|
|
1699
|
+
child.kill(signal);
|
|
1700
|
+
} catch (error) {
|
|
1701
|
+
finish(isEsrch(error));
|
|
1702
|
+
}
|
|
1703
|
+
if (!settled) timer = setTimeout(() => finish(Boolean(child.pid && !isAlive(child.pid))), timeoutMs);
|
|
1704
|
+
});
|
|
1495
1705
|
}
|
|
1496
|
-
function
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1706
|
+
async function terminateChild(child, { alreadyExited, timeoutMs, isAlive, platform = process.platform, waitForGroup = false }) {
|
|
1707
|
+
let stopped = alreadyExited;
|
|
1708
|
+
let deadline = Date.now() + timeoutMs;
|
|
1709
|
+
if (!stopped) stopped = await signalAndWaitForExit(child, "SIGTERM", {
|
|
1710
|
+
timeoutMs,
|
|
1711
|
+
isAlive,
|
|
1712
|
+
platform
|
|
1713
|
+
});
|
|
1714
|
+
if (!stopped) {
|
|
1715
|
+
deadline = Date.now() + timeoutMs;
|
|
1716
|
+
stopped = await signalAndWaitForExit(child, "SIGKILL", {
|
|
1717
|
+
timeoutMs,
|
|
1718
|
+
isAlive,
|
|
1719
|
+
platform
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
if (stopped && waitForGroup && child.pid) {
|
|
1723
|
+
while ((0, ownership_claim_exports.processGroupAlive)(child.pid) && Date.now() < deadline) await cancellableSleep(25);
|
|
1724
|
+
stopped = !(0, ownership_claim_exports.processGroupAlive)(child.pid);
|
|
1725
|
+
}
|
|
1726
|
+
closeChildPipes(child);
|
|
1727
|
+
return stopped;
|
|
1503
1728
|
}
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
return candidatePids(bundleDirs(root));
|
|
1729
|
+
function describe(err) {
|
|
1730
|
+
return err?.message || String(err);
|
|
1507
1731
|
}
|
|
1508
|
-
|
|
1509
|
-
|
|
1732
|
+
const STOP_TIMEOUT_MS = 5e3;
|
|
1733
|
+
const STOP_POLL_MS = 100;
|
|
1734
|
+
function isEsrch(err) {
|
|
1735
|
+
return err?.code === "ESRCH";
|
|
1510
1736
|
}
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1737
|
+
async function stopTunnel(record, { isAlive = pidExists, inspectIdentity = process_identity_exports.inspectProcessIdentity, platform = process.platform, operation, kill = (pid) => signalProcessTreeAsync(pid, "SIGTERM", {
|
|
1738
|
+
platform,
|
|
1739
|
+
operation
|
|
1740
|
+
}), now = Date.now, sleep = operation ? (ms) => cancellableSleep(ms, {
|
|
1741
|
+
signal: operation.signal,
|
|
1742
|
+
what: "tunnel stop"
|
|
1743
|
+
}) : defaultSleep, timeoutMs = STOP_TIMEOUT_MS } = {}) {
|
|
1744
|
+
operation?.signal.throwIfAborted();
|
|
1745
|
+
const missing = () => {
|
|
1746
|
+
removeRecordedTunnelLogFile(record?.logFile);
|
|
1747
|
+
return { status: "missing" };
|
|
1748
|
+
};
|
|
1749
|
+
const pid = record?.pid;
|
|
1750
|
+
if (!pid || !isAlive(pid)) return missing();
|
|
1751
|
+
if (!record.processToken) return {
|
|
1752
|
+
status: "failed",
|
|
1753
|
+
reason: `tunnel pid ${pid} has no process identity token, so Stim cannot verify ownership. Keep the record. Inspect the process, stop it with the provider tooling, remove the stale metroTunnel state, and retry.`
|
|
1754
|
+
};
|
|
1755
|
+
const identity = inspectIdentity(record);
|
|
1756
|
+
if (identity === "gone" || identity === "different") return missing();
|
|
1757
|
+
if (identity !== "same") return {
|
|
1758
|
+
status: "failed",
|
|
1759
|
+
reason: `could not verify the process identity for tunnel pid ${pid}; refusing to signal it.`
|
|
1760
|
+
};
|
|
1761
|
+
try {
|
|
1762
|
+
await kill(pid);
|
|
1763
|
+
} catch (err) {
|
|
1764
|
+
if (err instanceof WorkspaceProcessCleanupError) throw err;
|
|
1765
|
+
operation?.signal.throwIfAborted();
|
|
1766
|
+
return isEsrch(err) ? missing() : {
|
|
1767
|
+
status: "failed",
|
|
1768
|
+
reason: describe(err)
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
const deadline = now() + timeoutMs;
|
|
1772
|
+
const exited = () => {
|
|
1773
|
+
if (!isAlive(pid)) return true;
|
|
1774
|
+
const current = inspectIdentity(record);
|
|
1775
|
+
return current === "gone" || current === "different";
|
|
1776
|
+
};
|
|
1777
|
+
while (now() < deadline && !exited()) {
|
|
1778
|
+
operation?.signal.throwIfAborted();
|
|
1779
|
+
await sleep(STOP_POLL_MS);
|
|
1780
|
+
}
|
|
1781
|
+
operation?.signal.throwIfAborted();
|
|
1782
|
+
if (!exited()) return {
|
|
1783
|
+
status: "failed",
|
|
1784
|
+
reason: `pid ${pid} did not exit within ${timeoutMs}ms.`
|
|
1785
|
+
};
|
|
1786
|
+
removeRecordedTunnelLogFile(record.logFile);
|
|
1787
|
+
return { status: "stopped" };
|
|
1788
|
+
}
|
|
1789
|
+
//#endregion
|
|
1790
|
+
//#region src/supervisor/ownership.ts
|
|
1791
|
+
function resolveSupervisorTarget({ state, record, reservedPort, isAlive = pidExists, inspectIdentity = process_identity_exports.inspectProcessIdentity } = {}) {
|
|
1792
|
+
const statePid = numberOrNull(state?.pid);
|
|
1793
|
+
const recordPid = numberOrNull(record?.pid);
|
|
1794
|
+
const pid = statePid ?? recordPid;
|
|
1795
|
+
if (!pid) return { status: "none" };
|
|
1796
|
+
if (statePid && recordPid && statePid !== recordPid) {
|
|
1797
|
+
const gone = (entry) => ["gone", "different"].includes(inspectIdentity(entry));
|
|
1798
|
+
if (gone(state) && gone(record)) return {
|
|
1799
|
+
status: "stale",
|
|
1800
|
+
pid
|
|
1801
|
+
};
|
|
1802
|
+
return {
|
|
1803
|
+
status: "unverified",
|
|
1526
1804
|
pid,
|
|
1527
|
-
|
|
1528
|
-
startedAtMicros: start.startedAtMicros
|
|
1805
|
+
reason: `workspace state.json records supervisor pid ${statePid} but the registry records pid ${recordPid}`
|
|
1529
1806
|
};
|
|
1530
|
-
let open;
|
|
1531
|
-
try {
|
|
1532
|
-
open = getExecutor().runFile("lsof", [
|
|
1533
|
-
"-a",
|
|
1534
|
-
"-p",
|
|
1535
|
-
String(pid),
|
|
1536
|
-
"-d",
|
|
1537
|
-
"txt",
|
|
1538
|
-
"-Fn"
|
|
1539
|
-
], { timeoutMs: 1e4 });
|
|
1540
|
-
} catch (error) {
|
|
1541
|
-
if (["gone", "different"].includes((0, process_identity_exports.inspectProcessIdentity)(record))) continue;
|
|
1542
|
-
open = String(error.stdout ?? "");
|
|
1543
|
-
if (!open.trim()) throw unverified(pid, `lsof failed (${error.message.split("\n")[0]})`);
|
|
1544
|
-
}
|
|
1545
|
-
if (lsofNamesBundleExecutable(open, dirs)) instances.push(record);
|
|
1546
1807
|
}
|
|
1547
|
-
return
|
|
1808
|
+
if (!isAlive(pid)) return {
|
|
1809
|
+
status: "stale",
|
|
1810
|
+
pid
|
|
1811
|
+
};
|
|
1812
|
+
const port = numberOrNull(state?.port) ?? numberOrNull(record?.port);
|
|
1813
|
+
if (reservedPort !== null && reservedPort !== void 0 && port !== null && port !== reservedPort) return {
|
|
1814
|
+
status: "unverified",
|
|
1815
|
+
pid,
|
|
1816
|
+
reason: `supervisor pid ${pid} records port ${port}, but this project reserved port ${reservedPort}`
|
|
1817
|
+
};
|
|
1818
|
+
if (reservedPort !== null && reservedPort !== void 0 && port === null) return {
|
|
1819
|
+
status: "unverified",
|
|
1820
|
+
pid,
|
|
1821
|
+
reason: `supervisor pid ${pid} has no recorded port, so it cannot be matched against reserved port ${reservedPort}`
|
|
1822
|
+
};
|
|
1823
|
+
const identityRecord = statePid ? state : record;
|
|
1824
|
+
if (statePid && recordPid && state?.processToken !== record?.processToken) return {
|
|
1825
|
+
status: "unverified",
|
|
1826
|
+
pid,
|
|
1827
|
+
reason: "workspace state and registry process identities disagree"
|
|
1828
|
+
};
|
|
1829
|
+
const identity = inspectIdentity(identityRecord);
|
|
1830
|
+
if (identity === "gone" || identity === "different") return {
|
|
1831
|
+
status: "stale",
|
|
1832
|
+
pid
|
|
1833
|
+
};
|
|
1834
|
+
if (identity !== "same") return {
|
|
1835
|
+
status: "unverified",
|
|
1836
|
+
pid,
|
|
1837
|
+
reason: "the recorded process identity could not be verified"
|
|
1838
|
+
};
|
|
1839
|
+
return {
|
|
1840
|
+
status: "ours",
|
|
1841
|
+
processToken: identityRecord.processToken,
|
|
1842
|
+
pid,
|
|
1843
|
+
port,
|
|
1844
|
+
mode: state?.mode ?? record?.mode ?? null,
|
|
1845
|
+
startedAt: state?.startedAt ?? record?.startedAt ?? null
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
function numberOrNull(v) {
|
|
1849
|
+
return typeof v === "number" && Number.isSafeInteger(v) && v > 0 ? v : null;
|
|
1548
1850
|
}
|
|
1549
1851
|
//#endregion
|
|
1550
1852
|
//#region src/workspace/in-use.ts
|
|
1551
1853
|
const NATIVE_RUN = "native-run";
|
|
1552
1854
|
const NATIVE_RUN_HELD = "a stim ios, android or stop run holds its native-run.lock";
|
|
1553
|
-
function workspaceInUse(root, { supervisor = true, managedLocks = true, hosted = true, nativeRun = true } = {}) {
|
|
1855
|
+
function workspaceInUse(root, { supervisor = true, managedLocks = true, hosted = true, nativeRun = true } = {}, bundleInstances = bundleInstancePids) {
|
|
1554
1856
|
const reasons = [];
|
|
1555
1857
|
if (supervisor) {
|
|
1556
1858
|
const project = getProject(root);
|
|
@@ -1566,7 +1868,7 @@ function workspaceInUse(root, { supervisor = true, managedLocks = true, hosted =
|
|
|
1566
1868
|
else if (macos?.state === "running" || macos?.state === "orphaned" || macos?.build.state === "running") reasons.push("its macOS app or build is running");
|
|
1567
1869
|
else if (macos?.state === "unverified" || !macos && readWorkspaceState$1(root)?.macos !== void 0) reasons.push("its macOS process owner cannot be verified");
|
|
1568
1870
|
else try {
|
|
1569
|
-
const pids =
|
|
1871
|
+
const pids = bundleInstances(root);
|
|
1570
1872
|
if (pids.length) reasons.push(`its macOS app is running (pid ${pids.join(", ")})`);
|
|
1571
1873
|
} catch (error) {
|
|
1572
1874
|
reasons.push(`its running macOS apps cannot be listed: ${error.message}`);
|
|
@@ -1595,20 +1897,44 @@ function workspaceInUse(root, { supervisor = true, managedLocks = true, hosted =
|
|
|
1595
1897
|
if (managedLocks) reasons.push(...managedLockHolders(root));
|
|
1596
1898
|
return reasons;
|
|
1597
1899
|
}
|
|
1598
|
-
function
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1900
|
+
async function workspaceInUseAsync(root, checks = {}, signal, operation) {
|
|
1901
|
+
let instances;
|
|
1902
|
+
try {
|
|
1903
|
+
instances = checks.supervisor === false ? [] : await bundleInstancePidsAsync(root, signal, operation);
|
|
1904
|
+
} catch (error) {
|
|
1905
|
+
if (error instanceof WorkspaceProcessCleanupError) throw error;
|
|
1906
|
+
signal?.throwIfAborted();
|
|
1907
|
+
instances = error instanceof Error ? error : new Error(String(error));
|
|
1602
1908
|
}
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1909
|
+
signal?.throwIfAborted();
|
|
1910
|
+
return workspaceInUse(root, checks, () => {
|
|
1911
|
+
if (instances instanceof Error) throw instances;
|
|
1912
|
+
return instances;
|
|
1606
1913
|
});
|
|
1607
1914
|
}
|
|
1608
|
-
async function
|
|
1915
|
+
async function emptyWorkspaceDir(dir, signal) {
|
|
1916
|
+
signal?.throwIfAborted();
|
|
1917
|
+
const stat = await lstat(dir);
|
|
1918
|
+
signal?.throwIfAborted();
|
|
1919
|
+
if (stat.isSymbolicLink()) {
|
|
1920
|
+
await unlink(dir);
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
for (const name of await readdir(dir)) {
|
|
1924
|
+
signal?.throwIfAborted();
|
|
1925
|
+
if (name !== `${NATIVE_RUN}.lock`) await rm(join(dir, name), {
|
|
1926
|
+
recursive: true,
|
|
1927
|
+
force: true
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
async function withIdleWorkspace(root, fn, { purpose, operation, signal = operation?.signal, ...checks }) {
|
|
1609
1932
|
try {
|
|
1610
1933
|
return await withWorkspaceProcessLock(workspaceDir(root), NATIVE_RUN, async () => {
|
|
1611
|
-
const reasons =
|
|
1934
|
+
const reasons = signal ? await workspaceInUseAsync(root, {
|
|
1935
|
+
...checks,
|
|
1936
|
+
nativeRun: false
|
|
1937
|
+
}, signal, operation) : workspaceInUse(root, {
|
|
1612
1938
|
...checks,
|
|
1613
1939
|
nativeRun: false
|
|
1614
1940
|
});
|
|
@@ -1623,9 +1949,13 @@ async function withIdleWorkspace(root, fn, { purpose, ...checks }) {
|
|
|
1623
1949
|
}, {
|
|
1624
1950
|
external: true,
|
|
1625
1951
|
waitMs: 0,
|
|
1626
|
-
ownerPurpose: purpose
|
|
1952
|
+
ownerPurpose: purpose,
|
|
1953
|
+
signal,
|
|
1954
|
+
declareSpawnsFor: operation
|
|
1627
1955
|
});
|
|
1628
1956
|
} catch (error) {
|
|
1957
|
+
if (error instanceof WorkspaceProcessCleanupError) throw error;
|
|
1958
|
+
signal?.throwIfAborted();
|
|
1629
1959
|
if (workspaceProcessLockError(error)) return {
|
|
1630
1960
|
ran: false,
|
|
1631
1961
|
reasons: [NATIVE_RUN_HELD]
|
|
@@ -1656,17 +1986,23 @@ function readRecordedRoot(dir, name) {
|
|
|
1656
1986
|
try {
|
|
1657
1987
|
parsed = readJsonFile(join$1(dir, "workspace.json"));
|
|
1658
1988
|
} catch (error) {
|
|
1659
|
-
return
|
|
1660
|
-
projectRoot: null,
|
|
1661
|
-
problem: error?.code === "ENOENT" ? "it has no workspace.json" : "its workspace.json does not parse"
|
|
1662
|
-
};
|
|
1989
|
+
return unreadableRecordedRoot(error);
|
|
1663
1990
|
}
|
|
1991
|
+
return recordedRoot(parsed, name);
|
|
1992
|
+
}
|
|
1993
|
+
function unreadableRecordedRoot(error) {
|
|
1994
|
+
return {
|
|
1995
|
+
projectRoot: null,
|
|
1996
|
+
problem: error?.code === "ENOENT" ? "it has no workspace.json" : "its workspace.json does not parse"
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
function recordedRoot(parsed, name) {
|
|
1664
2000
|
const projectRoot = isJsonObject(parsed) ? parsed.projectRoot : void 0;
|
|
1665
2001
|
if (typeof projectRoot !== "string" || !isAbsolute$1(projectRoot)) return {
|
|
1666
2002
|
projectRoot: null,
|
|
1667
2003
|
problem: "its workspace.json records no absolute project root"
|
|
1668
2004
|
};
|
|
1669
|
-
if (workspaceName(projectRoot) !== name) return {
|
|
2005
|
+
if (workspaceName$1(projectRoot) !== name) return {
|
|
1670
2006
|
projectRoot: null,
|
|
1671
2007
|
problem: `its workspace.json records ${projectRoot}, whose workspace directory has another name`
|
|
1672
2008
|
};
|
|
@@ -1698,6 +2034,42 @@ function listWorkspaceDirs() {
|
|
|
1698
2034
|
}
|
|
1699
2035
|
return entries;
|
|
1700
2036
|
}
|
|
2037
|
+
async function listWorkspaceDirsAsync(signal) {
|
|
2038
|
+
signal?.throwIfAborted();
|
|
2039
|
+
const root = workspacesRoot();
|
|
2040
|
+
let names;
|
|
2041
|
+
try {
|
|
2042
|
+
names = (await readdir(root)).toSorted();
|
|
2043
|
+
} catch {
|
|
2044
|
+
signal?.throwIfAborted();
|
|
2045
|
+
return [];
|
|
2046
|
+
}
|
|
2047
|
+
const entries = [];
|
|
2048
|
+
for (const name of names) {
|
|
2049
|
+
signal?.throwIfAborted();
|
|
2050
|
+
const dir = join$1(root, name);
|
|
2051
|
+
try {
|
|
2052
|
+
if (!(await stat(dir)).isDirectory()) continue;
|
|
2053
|
+
} catch {
|
|
2054
|
+
signal?.throwIfAborted();
|
|
2055
|
+
continue;
|
|
2056
|
+
}
|
|
2057
|
+
signal?.throwIfAborted();
|
|
2058
|
+
let recorded;
|
|
2059
|
+
try {
|
|
2060
|
+
recorded = recordedRoot(JSON.parse(await readFile(join$1(dir, "workspace.json"), "utf8")), name);
|
|
2061
|
+
} catch (error) {
|
|
2062
|
+
signal?.throwIfAborted();
|
|
2063
|
+
recorded = unreadableRecordedRoot(error);
|
|
2064
|
+
}
|
|
2065
|
+
signal?.throwIfAborted();
|
|
2066
|
+
entries.push({
|
|
2067
|
+
dir,
|
|
2068
|
+
...recorded
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
return entries;
|
|
2072
|
+
}
|
|
1701
2073
|
function classifyWorkspaceDirs(entries, { registryKeys, exists, isMounted, inUse }) {
|
|
1702
2074
|
const orphaned = [];
|
|
1703
2075
|
const skipped = [];
|
|
@@ -1753,15 +2125,45 @@ function rootPresence(path) {
|
|
|
1753
2125
|
return code === "ENOENT" || code === "ENOTDIR" ? false : null;
|
|
1754
2126
|
}
|
|
1755
2127
|
}
|
|
1756
|
-
function collectOrphanedWorkspaces(registryKeys, mountedVolumes, { measure = true } = {}) {
|
|
1757
|
-
const classified =
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
2128
|
+
async function collectOrphanedWorkspaces(registryKeys, mountedVolumes, { measure = true, operation } = {}) {
|
|
2129
|
+
const classified = {
|
|
2130
|
+
orphaned: [],
|
|
2131
|
+
skipped: []
|
|
2132
|
+
};
|
|
2133
|
+
for (const entry of await listWorkspaceDirsAsync(operation?.signal)) {
|
|
2134
|
+
operation?.signal.throwIfAborted();
|
|
2135
|
+
const current = classifyWorkspaceDirs([entry], {
|
|
2136
|
+
registryKeys,
|
|
2137
|
+
exists: rootPresence,
|
|
2138
|
+
isMounted: (path) => isOnMountedVolume(path, mountedVolumes),
|
|
2139
|
+
inUse: () => []
|
|
2140
|
+
});
|
|
2141
|
+
classified.skipped.push(...current.skipped);
|
|
2142
|
+
for (const orphan of current.orphaned) {
|
|
2143
|
+
const reasons = operation ? await workspaceInUseAsync(orphan.projectRoot, {}, operation.signal, operation) : workspaceInUse(orphan.projectRoot);
|
|
2144
|
+
if (reasons.length) classified.skipped.push({
|
|
2145
|
+
dir: orphan.dir,
|
|
2146
|
+
reason: `its project root ${orphan.projectRoot} is gone, but ${reasons.join("; ")}`
|
|
2147
|
+
});
|
|
2148
|
+
else classified.orphaned.push(orphan);
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
if (!measure) return classified;
|
|
2152
|
+
const orphaned = [];
|
|
2153
|
+
for (const entry of classified.orphaned) {
|
|
2154
|
+
operation?.signal.throwIfAborted();
|
|
2155
|
+
const bytes = await measuredDirectorySizeAsync(entry.dir, {
|
|
2156
|
+
timeoutMs: SIZE_TIMEOUT_MS,
|
|
2157
|
+
operation,
|
|
2158
|
+
signal: operation?.signal
|
|
2159
|
+
});
|
|
2160
|
+
orphaned.push({
|
|
2161
|
+
...entry,
|
|
2162
|
+
bytes
|
|
2163
|
+
});
|
|
2164
|
+
}
|
|
1763
2165
|
return {
|
|
1764
|
-
orphaned
|
|
2166
|
+
orphaned,
|
|
1765
2167
|
skipped: classified.skipped
|
|
1766
2168
|
};
|
|
1767
2169
|
}
|
|
@@ -1789,9 +2191,6 @@ function planWorkspaceOutputs(entries, { olderThan, now }) {
|
|
|
1789
2191
|
});
|
|
1790
2192
|
}
|
|
1791
2193
|
const SIZE_TIMEOUT_MS = 6e4;
|
|
1792
|
-
function sizeOf(dir, timeoutMs = SIZE_TIMEOUT_MS) {
|
|
1793
|
-
return measuredDirectorySize(dir, { timeoutMs });
|
|
1794
|
-
}
|
|
1795
2194
|
function outputPaths(dir) {
|
|
1796
2195
|
const paths = WORKSPACE_OUTPUT_DIRS.map((name) => join$1(dir, name)).filter((path) => existsSync$1(path));
|
|
1797
2196
|
const macos = join$1(dir, "macos");
|
|
@@ -1801,39 +2200,58 @@ function outputPaths(dir) {
|
|
|
1801
2200
|
} catch {}
|
|
1802
2201
|
return paths;
|
|
1803
2202
|
}
|
|
1804
|
-
function outputBytes(paths, timeoutMs) {
|
|
2203
|
+
async function outputBytes(paths, timeoutMs, signal, operation) {
|
|
1805
2204
|
let total = 0;
|
|
1806
2205
|
for (const path of paths) {
|
|
1807
|
-
const size =
|
|
2206
|
+
const size = await measuredDirectorySizeAsync(path, {
|
|
2207
|
+
timeoutMs,
|
|
2208
|
+
signal,
|
|
2209
|
+
operation
|
|
2210
|
+
});
|
|
1808
2211
|
if (size === null) return null;
|
|
1809
2212
|
total += size;
|
|
1810
2213
|
}
|
|
1811
2214
|
return total;
|
|
1812
2215
|
}
|
|
1813
|
-
function collectWorkspaceOutputs({ olderThan, now, exclude = [], measure = true, sizeTimeoutMs = SIZE_TIMEOUT_MS }) {
|
|
2216
|
+
function collectWorkspaceOutputs({ olderThan, now, exclude = [], measure = true, sizeTimeoutMs = SIZE_TIMEOUT_MS, signal, operation }) {
|
|
1814
2217
|
const mountedVolumes = listMountedVolumes();
|
|
1815
|
-
const
|
|
1816
|
-
|
|
1817
|
-
inUse: entry.projectRoot === null ? [] : workspaceInUse(entry.projectRoot)
|
|
1818
|
-
}));
|
|
1819
|
-
return {
|
|
2218
|
+
const outputEntries = (entries) => entries.filter((entry) => !exclude.includes(entry.dir)).filter((entry) => entry.projectRoot === null || isOnMountedVolume(entry.projectRoot, mountedVolumes)).map((entry) => Object.assign({}, entry, { paths: outputPaths(entry.dir) })).filter((entry) => entry.paths.length > 0);
|
|
2219
|
+
const report = (sizes) => ({
|
|
1820
2220
|
root: workspacesRoot(),
|
|
1821
|
-
workspaces: planWorkspaceOutputs(
|
|
2221
|
+
workspaces: planWorkspaceOutputs(sizes.map((entry) => Object.assign({}, entry, { lastUsed: entry.projectRoot === null ? NaN : workspaceLastUsed(entry.projectRoot) })), {
|
|
1822
2222
|
olderThan,
|
|
1823
2223
|
now
|
|
1824
2224
|
})
|
|
1825
|
-
};
|
|
2225
|
+
});
|
|
2226
|
+
if (!measure && !signal) return report(outputEntries(listWorkspaceDirs()).map(({ paths: _paths, ...entry }) => Object.assign({}, entry, {
|
|
2227
|
+
bytes: null,
|
|
2228
|
+
inUse: entry.projectRoot === null ? [] : workspaceInUse(entry.projectRoot)
|
|
2229
|
+
})));
|
|
2230
|
+
return (async () => {
|
|
2231
|
+
const sizes = [];
|
|
2232
|
+
const entries = outputEntries(await listWorkspaceDirsAsync(signal));
|
|
2233
|
+
for (const { paths, ...entry } of entries) {
|
|
2234
|
+
signal?.throwIfAborted();
|
|
2235
|
+
sizes.push({
|
|
2236
|
+
...entry,
|
|
2237
|
+
bytes: measure ? await outputBytes(paths, sizeTimeoutMs, signal, operation) : null,
|
|
2238
|
+
inUse: entry.projectRoot === null ? [] : signal ? await workspaceInUseAsync(entry.projectRoot, {}, signal, operation) : workspaceInUse(entry.projectRoot)
|
|
2239
|
+
});
|
|
2240
|
+
}
|
|
2241
|
+
return report(sizes);
|
|
2242
|
+
})();
|
|
1826
2243
|
}
|
|
1827
|
-
async function clearWorkspaceOutputs(report, { olderThan, now = Date.now() }) {
|
|
2244
|
+
async function clearWorkspaceOutputs(report, { olderThan, now = Date.now(), operation, signal = operation?.signal }, { log, record } = GC_OUTPUT) {
|
|
1828
2245
|
let failures = 0;
|
|
1829
2246
|
let cleared = 0;
|
|
1830
2247
|
const clearedRoots = [];
|
|
1831
2248
|
for (const entry of report.workspaces) {
|
|
2249
|
+
signal?.throwIfAborted();
|
|
1832
2250
|
const root = entry.projectRoot;
|
|
1833
2251
|
if (!entry.willClear || root === null) {
|
|
1834
2252
|
if (entry.keptReason) {
|
|
1835
|
-
|
|
1836
|
-
|
|
2253
|
+
log(chalk.dim(`Kept the build outputs of ${root ?? entry.dir}: ${entry.keptReason}`));
|
|
2254
|
+
record("workspaceOutputs", "kept", root ?? entry.dir, {
|
|
1837
2255
|
bytes: entry.bytes,
|
|
1838
2256
|
detail: entry.keptReason
|
|
1839
2257
|
});
|
|
@@ -1842,7 +2260,7 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now() }) {
|
|
|
1842
2260
|
}
|
|
1843
2261
|
let run;
|
|
1844
2262
|
try {
|
|
1845
|
-
run = await withIdleWorkspace(root, () => {
|
|
2263
|
+
run = await withIdleWorkspace(root, async () => {
|
|
1846
2264
|
const [again] = planWorkspaceOutputs([{
|
|
1847
2265
|
...entry,
|
|
1848
2266
|
problem: null,
|
|
@@ -1853,16 +2271,26 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now() }) {
|
|
|
1853
2271
|
now
|
|
1854
2272
|
});
|
|
1855
2273
|
if (again?.keptReason) return again.keptReason;
|
|
1856
|
-
for (const path of outputPaths(entry.dir))
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
2274
|
+
for (const path of outputPaths(entry.dir)) {
|
|
2275
|
+
signal?.throwIfAborted();
|
|
2276
|
+
await rm(path, {
|
|
2277
|
+
recursive: true,
|
|
2278
|
+
force: true
|
|
2279
|
+
});
|
|
2280
|
+
}
|
|
2281
|
+
signal?.throwIfAborted();
|
|
1860
2282
|
return null;
|
|
1861
|
-
}, {
|
|
2283
|
+
}, {
|
|
2284
|
+
purpose: "gc",
|
|
2285
|
+
signal,
|
|
2286
|
+
operation
|
|
2287
|
+
});
|
|
1862
2288
|
} catch (error) {
|
|
2289
|
+
if (error instanceof WorkspaceProcessCleanupError) throw error;
|
|
2290
|
+
signal?.throwIfAborted();
|
|
1863
2291
|
failures++;
|
|
1864
|
-
|
|
1865
|
-
|
|
2292
|
+
log(chalk.red(`Could not clear the build outputs of ${root}: ${error?.message || String(error)}`));
|
|
2293
|
+
record("workspaceOutputs", "failed", root, {
|
|
1866
2294
|
bytes: entry.bytes,
|
|
1867
2295
|
detail: error?.message || String(error)
|
|
1868
2296
|
});
|
|
@@ -1870,8 +2298,8 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now() }) {
|
|
|
1870
2298
|
}
|
|
1871
2299
|
const kept = run.ran ? run.value : `in use: ${run.reasons.join("; ")}`;
|
|
1872
2300
|
if (kept) {
|
|
1873
|
-
|
|
1874
|
-
|
|
2301
|
+
log(chalk.yellow(`Kept the build outputs of ${root}: ${kept}`));
|
|
2302
|
+
record("workspaceOutputs", "kept", root, {
|
|
1875
2303
|
bytes: entry.bytes,
|
|
1876
2304
|
detail: kept
|
|
1877
2305
|
});
|
|
@@ -1879,10 +2307,10 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now() }) {
|
|
|
1879
2307
|
}
|
|
1880
2308
|
cleared += entry.bytes ?? 0;
|
|
1881
2309
|
clearedRoots.push(root);
|
|
1882
|
-
|
|
1883
|
-
|
|
2310
|
+
log(chalk.green(`Cleared the build outputs of ${root} (${sizeText(entry.bytes)})`));
|
|
2311
|
+
record("workspaceOutputs", "done", root, { bytes: entry.bytes });
|
|
1884
2312
|
}
|
|
1885
|
-
if (cleared)
|
|
2313
|
+
if (cleared) log(chalk.dim(`Cleared ${formatBytes(cleared)} of workspace build outputs. ${REBUILD_COST}`));
|
|
1886
2314
|
return {
|
|
1887
2315
|
failures,
|
|
1888
2316
|
cleared: clearedRoots
|
|
@@ -1892,47 +2320,56 @@ function stillOrphaned({ dir, projectRoot }) {
|
|
|
1892
2320
|
if (readRecordedRoot(dir, basename$1(dir)).projectRoot !== projectRoot || rootPresence(projectRoot) !== false || !isOnMountedVolume(projectRoot)) return false;
|
|
1893
2321
|
return !Object.keys(loadConfig$1()?.projects ?? {}).some((key) => isPathPrefix(projectRoot, key));
|
|
1894
2322
|
}
|
|
1895
|
-
async function deleteOrphanedWorkspaces(orphaned) {
|
|
2323
|
+
async function deleteOrphanedWorkspaces(orphaned, output = GC_OUTPUT, operation) {
|
|
1896
2324
|
let failures = 0;
|
|
1897
2325
|
for (const entry of orphaned) {
|
|
2326
|
+
operation?.signal.throwIfAborted();
|
|
1898
2327
|
if (!existsSync$1(entry.dir)) continue;
|
|
1899
2328
|
let run;
|
|
1900
2329
|
try {
|
|
1901
2330
|
run = await withIdleWorkspace(entry.projectRoot, async () => {
|
|
1902
2331
|
if (!stillOrphaned(entry)) return false;
|
|
1903
|
-
const browser = await teardownOwnedBrowser(entry.projectRoot, {
|
|
2332
|
+
const browser = await teardownOwnedBrowser(entry.projectRoot, {
|
|
2333
|
+
deleteProfile: true,
|
|
2334
|
+
...operation ? { operation } : {}
|
|
2335
|
+
});
|
|
1904
2336
|
if (browser.status === "failed" || browser.status === "skipped") throw new Error(`its owned Chrome profile was kept: ${browser.reason}`);
|
|
1905
|
-
emptyWorkspaceDir(entry.dir);
|
|
2337
|
+
await emptyWorkspaceDir(entry.dir, operation?.signal);
|
|
1906
2338
|
return true;
|
|
1907
|
-
}, {
|
|
2339
|
+
}, {
|
|
2340
|
+
purpose: "gc",
|
|
2341
|
+
operation
|
|
2342
|
+
});
|
|
1908
2343
|
} catch (error) {
|
|
2344
|
+
if (error instanceof WorkspaceProcessCleanupError) throw error;
|
|
2345
|
+
operation?.signal.throwIfAborted();
|
|
1909
2346
|
failures++;
|
|
1910
|
-
|
|
1911
|
-
|
|
2347
|
+
output.log(chalk.red(`Could not remove ${entry.dir}: ${error?.message || error}`));
|
|
2348
|
+
output.record("workspaceDirectory", "failed", entry.dir, {
|
|
1912
2349
|
bytes: entry.bytes ?? null,
|
|
1913
2350
|
detail: error?.message || String(error)
|
|
1914
2351
|
});
|
|
1915
2352
|
continue;
|
|
1916
2353
|
}
|
|
1917
2354
|
if (!run.ran) {
|
|
1918
|
-
|
|
1919
|
-
|
|
2355
|
+
output.log(chalk.yellow(`Kept ${entry.dir}: ${run.reasons.join("; ")}`));
|
|
2356
|
+
output.record("workspaceDirectory", "kept", entry.dir, {
|
|
1920
2357
|
bytes: entry.bytes ?? null,
|
|
1921
2358
|
detail: run.reasons.join("; ")
|
|
1922
2359
|
});
|
|
1923
2360
|
continue;
|
|
1924
2361
|
}
|
|
1925
2362
|
if (!run.value) {
|
|
1926
|
-
|
|
1927
|
-
|
|
2363
|
+
output.log(chalk.yellow(`Kept ${entry.dir}: it can no longer be confirmed orphaned.`));
|
|
2364
|
+
output.record("workspaceDirectory", "kept", entry.dir, {
|
|
1928
2365
|
bytes: entry.bytes ?? null,
|
|
1929
2366
|
detail: "it can no longer be confirmed orphaned"
|
|
1930
2367
|
});
|
|
1931
2368
|
continue;
|
|
1932
2369
|
}
|
|
1933
2370
|
const size = entry.bytes === void 0 ? "" : ` (${sizeText(entry.bytes)})`;
|
|
1934
|
-
|
|
1935
|
-
|
|
2371
|
+
output.log(chalk.green(`Removed the orphaned workspace directory ${entry.dir}${size}`));
|
|
2372
|
+
output.record("workspaceDirectory", "done", entry.dir, { bytes: entry.bytes ?? null });
|
|
1936
2373
|
}
|
|
1937
2374
|
return failures;
|
|
1938
2375
|
}
|
|
@@ -1940,4 +2377,4 @@ function sizeText(bytes) {
|
|
|
1940
2377
|
return bytes === null ? "size unknown" : formatBytes(bytes);
|
|
1941
2378
|
}
|
|
1942
2379
|
//#endregion
|
|
1943
|
-
export {
|
|
2380
|
+
export { teardownParkedAvd as A, createGcResults as B, runningBundleInstancesAsync as C, teardownOwnedAvd as D, teardownBrowserHeld as E, writeUsageCache as F, acquireBuildSlot as I, listBuildSlots as L, probePublicHttp as M, canonical$1 as N, teardownOwnedBrowser as O, measureDu as P, releaseBuildSlot as R, withManagedTunnelRemovalLock as S, eraseParkedIosSim as T, takeGcResults as V, stopTunnel as _, collectWorkspaceOutputs as a, withManagedRemoteWorktreeRemovalLock as b, listWorkspaceDirs as c, emptyWorkspaceDir as d, withIdleWorkspace as f, startTunnelSequence as g, resolveSupervisorTarget as h, collectOrphanedWorkspaces as i, teardownParkedIosSim as j, teardownOwnedIosSim as k, rootPresence as l, workspaceInUseAsync as m, WORKSPACE_OUTPUT_DIRS as n, deleteOrphanedWorkspaces as o, workspaceInUse as p, clearWorkspaceOutputs as r, isInsideWorkspaces as s, REBUILD_COST as t, sizeText as u, terminateChild as v, eraseParkedAvd as w, withManagedTunnelLock as x, withManagedRemoteWorktreeLock as y, GC_OUTPUT as z };
|