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