stim 1.20.2 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +141 -8
- package/dist/acquire-artifact-Co2k4f7O.mjs +171 -0
- package/dist/act-DsuqEkiM.mjs +2801 -0
- package/dist/activity-CMLmzp7o.mjs +1397 -0
- package/dist/{agent-device-usage-output-Dzgv6lgR.mjs → agent-device-usage-output-DUFaTMdd.mjs} +23 -67
- package/dist/{android-DCKZrNvV.mjs → android-ByRMUoiJ.mjs} +157 -93
- package/dist/{android-Mz_Inn3w.mjs → android-C7sVmyI9.mjs} +299 -116
- package/dist/{android-Dlzxshgp.mjs → android-DGkNqa_c.mjs} +5 -5
- package/dist/{android-HqP1_vB_.mjs → android-DR5lZ2Az.mjs} +2 -2
- package/dist/android-cas-compiler.mjs +2 -2
- package/dist/{android-cas-Bl-cLJBk.mjs → android-cas-ngferVq2.mjs} +7 -6
- package/dist/android-project-BCAAmMwz.mjs +12 -0
- package/dist/android-project-DVWyCbWU.d.mts +391 -0
- package/dist/api.d.mts +810 -64
- package/dist/api.mjs +782 -1
- package/dist/{artifact-HSwkIWEQ.mjs → artifact-8EY8SAPw.mjs} +240 -213
- package/dist/{artifact-D3czlUvT.mjs → artifact-DJPy518Y.mjs} +203 -164
- package/dist/artifact-platform-CXcmX_j3.mjs +27 -0
- package/dist/{browser-web-CA-F1GQo.mjs → browser-web-Bcv99PBM.mjs} +194 -64
- package/dist/budget-Cvwn54qV.mjs +9285 -0
- package/dist/{build-DYDbfkk5.mjs → build-CabC3G23.mjs} +153 -98
- package/dist/build-cache-Dv7RN200.mjs +602 -0
- package/dist/build-plan-DUC8Ka9y.mjs +248 -0
- package/dist/{cache-manifest-XH-fvDUj.mjs → cache-manifest-DSeQjt-Q.mjs} +2 -2
- package/dist/cache-manifest.mjs +1 -1
- package/dist/{ccache-zdKiYQ3E.mjs → ccache-Btwc36-I.mjs} +10 -9
- package/dist/{cdp-Dq_ypAlJ.mjs → cdp-3fwICjzi.mjs} +18 -5
- package/dist/{chrome-BnzZMqsR.mjs → chrome-Bbw689-6.mjs} +7 -4
- package/dist/{cli-CLP3diBm.mjs → cli-CHZa9Lvk.mjs} +20 -20
- package/dist/cli.mjs +1 -1
- package/dist/client-OyRoEuSr.mjs +1577 -0
- package/dist/collector-run.d.mts +4 -40
- package/dist/collector-run.mjs +9 -8
- package/dist/{command-output-BTy8hZ0F.mjs → command-output-gwpm28fu.mjs} +1 -1
- package/dist/config-DlrsduwG.mjs +955 -0
- package/dist/{created-devices-Dzd5C2LF.mjs → created-devices-x9rN6bhs.mjs} +14 -4
- package/dist/{dependency-state-DG0p31-y.mjs → dependency-state-1I9KZT1Y.mjs} +20 -5
- package/dist/{detached-entry-B9C_zeQC.mjs → detached-entry-BwNEtIrP.mjs} +1 -1
- package/dist/{dev-client-DAWVIGBu.mjs → dev-client-BSDR5h3F.mjs} +2 -2
- package/dist/{device-BECOiufl.mjs → device-B-L3Fbxy.mjs} +91 -34
- package/dist/device-DklWZLqb.mjs +1068 -0
- package/dist/{device-capacity-CBUAcRyQ.mjs → device-capacity-DK5_EwRS.mjs} +42 -13
- package/dist/device-host-worker.mjs +76 -79
- package/dist/{device-ios-COSi6oS1.mjs → device-ios-DoB6KFCO.mjs} +77 -51
- package/dist/{device-lease-run-Cw2M4z51.mjs → device-lease-run-xJPQ_QQG.mjs} +14 -26
- package/dist/{device-pool-Bf-pOhSw.mjs → device-pool-BMkdsefq.mjs} +5 -5
- package/dist/{doctor-kfMBnUw6.mjs → doctor-Bo7BXLux.mjs} +271 -320
- package/dist/{doctor-CB8oZKCk.mjs → doctor-D5_EzwfA.mjs} +186 -91
- package/dist/doctor-fingerprint-DZLoYFVb.mjs +16 -0
- package/dist/doctor-fingerprint.mjs +19 -0
- package/dist/doctor-thjWAV0z.mjs +2 -0
- package/dist/{doctor-watchman-BRyV2adA.mjs → doctor-watchman-N2wDotwQ.mjs} +40 -21
- package/dist/{eas-build-JyfqyHfC.mjs → eas-build-CwknaQYj.mjs} +53 -20
- package/dist/{build-plan-B7SZsc2N.mjs → eas-fallback-CSgAulr5.mjs} +41 -214
- package/dist/{watchman-CPQFZLXY.mjs → eas-session-ledger-DlY_serQ.mjs} +230 -11
- package/dist/{error-diagnostics-BR677IDM.mjs → error-diagnostics-DBWO2rxE.mjs} +172 -82
- package/dist/exec-DEoAhcze.mjs +858 -0
- package/dist/exec-DnYc4HGH.d.mts +78 -0
- package/dist/{gc-qWOMdnER.mjs → gc-CXxN-4SE.mjs} +1091 -1029
- package/dist/gc-DnNDQyaP.mjs +3 -0
- package/dist/{guide-G9cAbVAc.mjs → guide-Br0LEfKy.mjs} +1165 -297
- package/dist/{hosted-android-C3MHGld1.mjs → hosted-android-B-9f-jkd.mjs} +10 -8
- package/dist/{hosted-client-ZEKpS9vw.mjs → hosted-client-DZAPrcro.mjs} +34 -19
- package/dist/{hosted-ios-l-o3iM9J.mjs → hosted-ios-CsCS21hn.mjs} +1 -1
- package/dist/{hosted-logs-EzQM7-3Z.mjs → hosted-logs-FIRx9X2f.mjs} +77 -43
- package/dist/{hosted-macos-BACVAVqE.mjs → hosted-macos-_awYSpfv.mjs} +35 -22
- package/dist/{hosted-native-A4s1QPXe.mjs → hosted-native-BdzE668-.mjs} +49 -27
- package/dist/idle-C4cZ757K.d.mts +24 -0
- package/dist/{idle-shutdown-DtxhPTKe.mjs → idle-shutdown-DDg3SdvY.mjs} +11 -10
- package/dist/ios-CDvlmUs7.d.mts +24 -0
- package/dist/{ios-BwVCF7oA.mjs → ios-Ds1clbIt.mjs} +8 -8
- package/dist/{ios-device-D7pi2Sca.mjs → ios-device--h_-8ENa.mjs} +51 -26
- package/dist/{ios-device-B5veSDDQ.mjs → ios-device-BXZ0c7Kb.mjs} +1 -1
- package/dist/{ios-RkW4pnQf.mjs → ios-hcBpnEGk.mjs} +200 -93
- package/dist/{ios-Ce7bc7br.mjs → ios-pm4nL6By.mjs} +160 -108
- package/dist/{ios-project-K9hY_Jab.mjs → ios-project-CZhKODF3.mjs} +13 -13
- package/dist/ios-signing-BtAffm0A.mjs +489 -0
- package/dist/{ios-state-CBLe1xYw.mjs → ios-state-Dtw5K2aI.mjs} +1 -1
- package/dist/{ios-nOLJGVdy.mjs → ios-xtlRHaeB.mjs} +1 -1
- package/dist/{js-swap-Bb3nsS07.mjs → js-swap-Bwhh4S7V.mjs} +30 -25
- package/dist/{launch-C3qJ6Agb.mjs → launch-BjxemNpK.mjs} +85 -543
- package/dist/{launch-F4sOqAhB.mjs → launch-CCzXlc8r.mjs} +91 -90
- package/dist/logs-BfwwadsZ.mjs +479 -0
- package/dist/logs-jTS_lHSo.mjs +248 -0
- package/dist/{macos-D7uDvO-z.mjs → macos-DVlSSyUg.mjs} +1 -1
- package/dist/macos-NfV2D7dO.mjs +694 -0
- package/dist/macos-run.mjs +1 -1
- package/dist/maintenance-run.mjs +123 -248
- package/dist/{metro-DnCEjCxs.mjs → metro-DiXtA3Ty.mjs} +107 -30
- package/dist/metro-gateway-CKZqKqx7.mjs +209 -0
- package/dist/{named-ports-4WopRPg-.mjs → named-ports-DkEzOfzO.mjs} +42 -90
- package/dist/native-android-CqgnOgsT.mjs +645 -0
- package/dist/native-gradle-artifact-inputs-DHFnmrVf.mjs +270 -0
- package/dist/native-gradle-inputs-Dnh84Zbd.mjs +211 -0
- package/dist/native-inputs-TRTTTRxt.mjs +91 -0
- package/dist/native-run-NwUuxbkg.mjs +276 -0
- package/dist/{native-runtime-k8dTIrbK.mjs → native-runtime-B7KIafpM.mjs} +11 -9
- package/dist/native-source-Y0yLhFyQ.mjs +133 -0
- package/dist/{native-xcode-inputs-CykopJdM.mjs → native-xcode-inputs-D_VOfFL7.mjs} +26 -18
- package/dist/{native-xcode-ios-Bbf30Gm0.mjs → native-xcode-ios-BYDOHWLN.mjs} +104 -60
- package/dist/{ndjson-GOnbyvso.mjs → ndjson-CgE_o3Dl.mjs} +3 -3
- package/dist/offload-worker.d.mts +59 -41
- package/dist/offload-worker.mjs +478 -115
- package/dist/operation-exec-BW0em61G.mjs +171 -0
- package/dist/{ownership-DCtw_2IA.mjs → ownership-C5fo5Edb.mjs} +3 -3
- package/dist/{page-DRHy329N.mjs → page-DBL2PiUU.mjs} +12 -5
- package/dist/paths-BsJMoPP2.mjs +12 -0
- package/dist/paths-DQtTgsBf.mjs +42 -0
- package/dist/{placement-log-MOfPVgaM.mjs → placement-log-BnGgJRRZ.mjs} +5 -5
- package/dist/{plan-placement-V0TdzYXp.mjs → plan-placement-DIsMtN-d.mjs} +15 -11
- package/dist/{ports-DdSUicVo.mjs → ports-Cy1RkmuQ.mjs} +3 -3
- package/dist/prebuild-D4JoEAEs.mjs +484 -0
- package/dist/{project-_phNIJfs.mjs → project-wM_XFTvE.mjs} +65 -37
- package/dist/{pull-request-CQ8sPbZa.mjs → pull-request-W1IuLXDt.mjs} +102 -20
- package/dist/pull-request-eODyIqZF.d.mts +124 -0
- package/dist/pull-requests.d.mts +2 -120
- package/dist/pull-requests.mjs +2 -2
- package/dist/{react-native-android-CM1XRDwv.mjs → react-native-android-6OZ7MSM7.mjs} +121 -72
- package/dist/{gradle-DSrXb6lG.mjs → react-native-build-Dvd8hC21.mjs} +292 -15
- package/dist/react-native-doctor-DsoSF1Y9.mjs +234 -0
- package/dist/{react-native-ios-Dhq_eiYQ.mjs → react-native-ios-4u1Rn-GR.mjs} +71 -39
- package/dist/{recordings-D0d5aQt0.mjs → recordings-CQmfzAh7.mjs} +12 -9
- package/dist/registration-BcRgF6Ox.mjs +51 -0
- package/dist/{reload-C8JhxjSh.mjs → reload-BzkYabB-.mjs} +94 -47
- package/dist/reload-D7AVlb8N.mjs +2 -0
- package/dist/{remote-BiR6aYjy.mjs → remote-DjGDmlTZ.mjs} +26 -18
- package/dist/result-D2Ykz_iD.mjs +435 -0
- package/dist/{run-BtPgpKks.mjs → run-DidHghD1.mjs} +5 -5
- package/dist/selection-DSMbWEX2.mjs +620 -0
- package/dist/{server-bare-R2m_gsH8.mjs → server-bare-CafzuyFB.mjs} +3 -4
- package/dist/{server-command-CTtCkmcQ.mjs → server-command-C6QjeupS.mjs} +14 -7
- package/dist/{server-expo-DsguMGUF.mjs → server-expo-CrjB29pq.mjs} +54 -11
- package/dist/server-expo-D58c9L0M.mjs +2 -0
- package/dist/{settings-B717odTD.mjs → settings-C7O8A6ZI.mjs} +309 -119
- package/dist/settings-DpR-1Wnn.mjs +114 -0
- package/dist/settings-write-CzD60rOD.mjs +360 -0
- package/dist/settings.schema.json +176 -16
- package/dist/{simslim-CCr3rE-A.mjs → simslim-BNckWpXi.mjs} +14 -12
- package/dist/{slot-launch-DPt0m9Nd.mjs → slot-launch-CbfmNgM6.mjs} +15 -20
- package/dist/{spawn-entry-Dcb7sDUz.mjs → spawn-entry-jw19m8Nl.mjs} +0 -1
- package/dist/stage-Bps5oius.mjs +200 -0
- package/dist/{start-TXeMmmO2.mjs → start-Br-oxqMD.mjs} +1 -1
- package/dist/start-EojN4WFz.mjs +1364 -0
- package/dist/{state-D1Icyjlu.mjs → state--smjFSTL.mjs} +55 -13
- package/dist/{state-CG_OFoaA.mjs → state-08I3hw3_.mjs} +3 -2
- package/dist/{stats-BoY5sEPU.mjs → stats-D3GerlFW.mjs} +7 -13
- package/dist/stats-report-BzaPrRm6.mjs +16 -0
- package/dist/{status-BTA7kKiQ.mjs → status-BRROykC8.mjs} +7 -5
- package/dist/status-EvrK72L2.mjs +2 -0
- package/dist/{status-WgFbkRzj.mjs → status-WBDajew2.mjs} +554 -452
- package/dist/{status-watch-C23ILtf9.mjs → status-watch-Bg_u-pwR.mjs} +222 -25
- package/dist/status-worker.d.mts +1 -0
- package/dist/status-worker.mjs +19 -0
- package/dist/stim-desktop-D5OGpYZM.mjs +112 -0
- package/dist/{stop-DR6VoDIk.mjs → stop-DTZ9uTw3.mjs} +2 -2
- package/dist/{stop-DPEhunlw.mjs → stop-Dky10AEJ.mjs} +33 -23
- package/dist/{stop-BNmBMi1F.mjs → stop-Mv6W9FAk.mjs} +137 -98
- package/dist/supervisor-run.d.mts +8 -17
- package/dist/supervisor-run.mjs +56 -28
- package/dist/{support-CwjbFs9H.mjs → support-CNioewsd.mjs} +14 -14
- package/dist/{support-DMTqj3Hm.mjs → support-yUiOjnVY.mjs} +20 -13
- package/dist/swiftpm-artifact-inputs-DFcGsXDP.mjs +153 -0
- package/dist/swiftpm-macos-BIak-Rxv.mjs +532 -0
- package/dist/toolchain-BY8X8HXx.mjs +2 -0
- package/dist/toolchain-DpJMHq0n.mjs +876 -0
- package/dist/{trigger-CigAYjeL.mjs → trigger-Cbyc41d_.mjs} +21 -6
- package/dist/trigger-yb_nFzPF.mjs +2 -0
- package/dist/{warm-progress-BuY-Fvfn.mjs → warm-progress-CA3TgT3-.mjs} +263 -156
- package/dist/web-CZ07gd77.mjs +2 -0
- package/dist/{web-BDBMxDpx.mjs → web-DKx6yoFA.mjs} +20 -14
- package/dist/web-run.mjs +13 -12
- package/dist/windows-report.d.mts +1 -0
- package/dist/windows-report.mjs +18 -0
- package/dist/windows-report.ps1 +43 -0
- package/dist/{workspace-state-DJt_MMvf.mjs → workspace-state-U08dEzhG.mjs} +62 -58
- package/dist/{workspaces-C36s2jb9.mjs → workspaces-CpANmBlq.mjs} +1723 -1286
- package/dist/{worktree-CgvJYJ-Q.mjs → worktree-CHecZ4Oq.mjs} +1097 -547
- package/dist/worktree-ZqZVjD9K.mjs +2 -0
- package/dist/xcode-BXn9-xYx.mjs +896 -0
- package/package.json +7 -5
- package/shim/android-audio/Main.java +24 -0
- package/shim/android-audio/README.md +17 -0
- package/shim/android-audio.jar +0 -0
- package/shim/android-optimizations.gradle +7 -5
- package/dist/activity-BSV745Kz.mjs +0 -629
- package/dist/api-qvOzWyfO.mjs +0 -154
- package/dist/api-run.mjs +0 -294
- package/dist/app-install-BIKtMfPq.mjs +0 -896
- package/dist/artifact-lifecycle-DzPpBYvK.mjs +0 -30
- package/dist/budget-f7516Zk0.mjs +0 -4106
- package/dist/build-facts-DcLv2HSd.d.mts +0 -111
- package/dist/cancellation-C_22VQTH.mjs +0 -24
- package/dist/client-qQAaXSUs.mjs +0 -1771
- package/dist/config-BYDxlMY3.mjs +0 -562
- package/dist/device-DuiPJQSn.mjs +0 -290
- package/dist/device-lease-BX1sJQ2E.mjs +0 -1224
- package/dist/device-remote-xRT0Vmo9.mjs +0 -1272
- package/dist/eas-session-ledger-Cm0Gwq2F.mjs +0 -207
- package/dist/exec-DqKo9GKJ.mjs +0 -268
- package/dist/git-DDfFzkMm.mjs +0 -45
- package/dist/launch-verify-B6Q8hZP-.mjs +0 -428
- package/dist/logs-BGrEUaEs.mjs +0 -383
- package/dist/macos-Ch5VJmcS.mjs +0 -584
- package/dist/metro-gateway-CX9Xlkq2.mjs +0 -103
- package/dist/miss-reason-mKzZ4VvN.mjs +0 -238
- package/dist/native-android-D5qc3-_v.mjs +0 -226
- package/dist/native-gradle-inputs-BsLRrl7u.mjs +0 -349
- package/dist/ndjson-Du3j0AQ9.d.mts +0 -17
- package/dist/plan-C4TjdPsR.mjs +0 -260
- package/dist/prebuild-DYbucPXy.mjs +0 -212
- package/dist/preview-78a9kR_-.mjs +0 -1011
- package/dist/react-native-build-CtCccJtb.mjs +0 -239
- package/dist/react-native-doctor-CfRnFfjs.mjs +0 -78
- package/dist/remote-cache-OyOyVIS3.mjs +0 -526
- package/dist/result-CaJg5Iat.mjs +0 -186
- package/dist/server-expo-Bigriskl.mjs +0 -2
- package/dist/settings-BORNaaCO.mjs +0 -402
- package/dist/settings-vk3mJGSm.d.mts +0 -13
- package/dist/spawn-claims-DVS5RkMS.mjs +0 -72
- package/dist/stage-KNTQopqv.mjs +0 -157
- package/dist/start-BWqzODpb.mjs +0 -1116
- package/dist/stim-desktop-BzBN2Em2.mjs +0 -63
- package/dist/swiftpm-macos-DRNVJWlb.mjs +0 -119
- package/dist/tailnet-Dh-qiYzG.mjs +0 -265
- package/dist/toolchain-BFkAcstd.mjs +0 -2
- package/dist/toolchain-DxSsNssu.mjs +0 -1657
- package/dist/trigger-9_USgmxz.mjs +0 -2
- package/dist/web-Dq-i3wzJ.mjs +0 -2
- package/dist/worktree-DpLdCHga.mjs +0 -2
- package/dist/{api-run.d.mts → doctor-fingerprint.d.mts} +0 -0
- package/dist/{errors-DzkMYh8Q.mjs → errors-ccaPc4ox.mjs} +2 -2
|
@@ -0,0 +1,1397 @@
|
|
|
1
|
+
import { c as declareSpawnsOn, f as stopDeclaringSpawnsOn, g as process_identity_exports, t as getExecutor, x as ownership_claim_exports } from "./exec-DEoAhcze.mjs";
|
|
2
|
+
import { m as workspaceLogsDir$1, v as withDirLock$1, y as withDirLockAsync$1 } from "./paths-DQtTgsBf.mjs";
|
|
3
|
+
import { U as parseDeviceSlotKey, V as deviceSlotKey, d as getConfigDir, q as validateDeviceSlot } from "./config-DlrsduwG.mjs";
|
|
4
|
+
import { a as formatElapsed, i as formatDuration, m as shortHash } from "./command-output-gwpm28fu.mjs";
|
|
5
|
+
import { n as runOperationFileQuiet } from "./operation-exec-BW0em61G.mjs";
|
|
6
|
+
import { c as withWorkspaceStateLock, d as writeWorkspaceState, l as withWorkspaceStateLockAsync, n as clearWorkspaceStateKeys, r as readWorkspaceState$1 } from "./workspace-state-U08dEzhG.mjs";
|
|
7
|
+
import { h as resolveBuild } from "./build-cache-Dv7RN200.mjs";
|
|
8
|
+
import { randomBytes } from "node:crypto";
|
|
9
|
+
import { closeSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { basename, join, resolve } from "node:path";
|
|
12
|
+
import { ambientContext } from "@stim-cli/core";
|
|
13
|
+
import { agentDeviceStateDirs, isJsonObject, readViewedDevices } from "@stim-cli/core/state";
|
|
14
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
15
|
+
//#region src/devices/automation-tools.ts
|
|
16
|
+
const AUTOMATION_TOOLS = [
|
|
17
|
+
{
|
|
18
|
+
tool: "agent-browser",
|
|
19
|
+
platforms: ["web"],
|
|
20
|
+
cdp: /agent-browser/i
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
tool: "agent-device",
|
|
24
|
+
platforms: [
|
|
25
|
+
"ios",
|
|
26
|
+
"android",
|
|
27
|
+
"web"
|
|
28
|
+
],
|
|
29
|
+
host: /agent-device/i,
|
|
30
|
+
cdp: /agent-device/i
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
tool: "argent",
|
|
34
|
+
platforms: [
|
|
35
|
+
"ios",
|
|
36
|
+
"android",
|
|
37
|
+
"web"
|
|
38
|
+
],
|
|
39
|
+
host: /\/bin\/[\w-]+\/(simulator-server(\.exe)?\s+(ios|android|android_device)|ax-service)\b|com\.argent\.androiddevtools/,
|
|
40
|
+
instrumentation: /com\.argent\.androiddevtools/,
|
|
41
|
+
cdp: /@swmansion\/argent\/dist\/tool-server/
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
tool: "idb",
|
|
45
|
+
platforms: ["ios"],
|
|
46
|
+
host: /idb_companion/
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
tool: "maestro",
|
|
50
|
+
platforms: ["ios", "android"],
|
|
51
|
+
host: /maestro/i
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
tool: "appium",
|
|
55
|
+
platforms: ["ios", "android"],
|
|
56
|
+
host: /appium|WebDriverAgent/i
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
tool: "xcodebuild",
|
|
60
|
+
platforms: ["ios"],
|
|
61
|
+
host: /\bxcodebuild\b.*\btest(-without-building)?\b/
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
tool: "simctl",
|
|
65
|
+
platforms: ["ios"],
|
|
66
|
+
host: /\bsimctl\s+(io|spawn)\b/
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
tool: "uiautomator",
|
|
70
|
+
platforms: ["android"],
|
|
71
|
+
instrumentation: /uiautomator/
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
tool: "instrumentation",
|
|
75
|
+
platforms: ["android"],
|
|
76
|
+
instrumentation: /androidx\.test|\binstrument\b/
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
tool: "chrome-devtools-mcp",
|
|
80
|
+
platforms: ["web"],
|
|
81
|
+
cdp: /chrome-devtools-mcp/i
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
tool: "playwright",
|
|
85
|
+
platforms: ["web"],
|
|
86
|
+
cdp: /playwright/i
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
tool: "puppeteer",
|
|
90
|
+
platforms: ["web"],
|
|
91
|
+
cdp: /puppeteer/i
|
|
92
|
+
}
|
|
93
|
+
];
|
|
94
|
+
const HOST_EXCLUSIONS = [/\bsimctl\s+spawn\b.*\blog\s+stream\b/, /\blogcat\b/];
|
|
95
|
+
/** Stim's own DevTools clients: the browser supervisor, `stim-frames` in stim-server, Stim Desktop, the stim CLI. */
|
|
96
|
+
const CDP_EXCLUSIONS = [/\bstim-(frames|server|web)\b|StimDesktop|\/stim(-cli)?\/(dist|bin)\/|\/bin\/stim(\s|$)|^stim(\s|$)/];
|
|
97
|
+
function namesDevice(command, id) {
|
|
98
|
+
const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
99
|
+
return new RegExp(`(^|[^A-Za-z0-9])${escaped}([^A-Za-z0-9]|$)`).test(command);
|
|
100
|
+
}
|
|
101
|
+
/** The tool a host process drives the device `id` with, or null when the command does not name the device. */
|
|
102
|
+
function hostDriverTool(command, platform, id) {
|
|
103
|
+
if (!namesDevice(command, id) || HOST_EXCLUSIONS.some((pattern) => pattern.test(command))) return null;
|
|
104
|
+
return AUTOMATION_TOOLS.find((entry) => entry.platforms.includes(platform) && entry.host?.test(command))?.tool ?? null;
|
|
105
|
+
}
|
|
106
|
+
/** The tool behind an on-device process's arguments, or null when it is not instrumentation. */
|
|
107
|
+
function instrumentationTool(args) {
|
|
108
|
+
return AUTOMATION_TOOLS.find((entry) => entry.platforms.includes("android") && entry.instrumentation?.test(args))?.tool ?? null;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The tool a DevTools client's command line names, or null for Stim's own clients. A client no entry names is
|
|
112
|
+
* reported by its script or executable name.
|
|
113
|
+
*/
|
|
114
|
+
function cdpClientTool(command) {
|
|
115
|
+
if (CDP_EXCLUSIONS.some((pattern) => pattern.test(command))) return null;
|
|
116
|
+
const known = AUTOMATION_TOOLS.find((entry) => entry.platforms.includes("web") && entry.cdp?.test(command));
|
|
117
|
+
if (known) return known.tool;
|
|
118
|
+
const [executable = "", script] = command.trim().split(/\s+/);
|
|
119
|
+
const name = basename(executable);
|
|
120
|
+
return (/^(node|bun|deno|python3?)$/.test(name) && script ? basename(script) : name) || "unknown DevTools client";
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/engine/build-lock.ts
|
|
124
|
+
const LOCK_SUFFIX$1 = ".lock";
|
|
125
|
+
const WAIT_POLL_MS = 1e3;
|
|
126
|
+
const WAIT_PROGRESS_MS = 3e4;
|
|
127
|
+
const WAIT_CEILING_MS = 54e5;
|
|
128
|
+
function sleepAsync(ms) {
|
|
129
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
130
|
+
}
|
|
131
|
+
function segment(value) {
|
|
132
|
+
return String(value).replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^\.+/, "") || "unknown";
|
|
133
|
+
}
|
|
134
|
+
function buildLocksDir() {
|
|
135
|
+
return join(getConfigDir(), "build-locks");
|
|
136
|
+
}
|
|
137
|
+
function buildLockPath(platform, key) {
|
|
138
|
+
return join(buildLocksDir(), `${segment(platform)}-${segment(key)}${LOCK_SUFFIX$1}`);
|
|
139
|
+
}
|
|
140
|
+
function label(platform) {
|
|
141
|
+
return `${platform} build`;
|
|
142
|
+
}
|
|
143
|
+
function toRecord(holder) {
|
|
144
|
+
const details = holder.details;
|
|
145
|
+
return {
|
|
146
|
+
pid: holder.owner.pid,
|
|
147
|
+
projectRoot: typeof details.projectRoot === "string" ? details.projectRoot : null,
|
|
148
|
+
startedAt: holder.startedAt || null,
|
|
149
|
+
logFile: typeof details.logFile === "string" ? details.logFile : null
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function readBuildLock(pathOrSpec) {
|
|
153
|
+
const path = typeof pathOrSpec === "string" ? pathOrSpec : buildLockPath(pathOrSpec.platform, pathOrSpec.key);
|
|
154
|
+
const survey = (0, ownership_claim_exports.readClaimSet)(path);
|
|
155
|
+
const holder = survey.live[0] ?? survey.dead[0];
|
|
156
|
+
return holder ? toRecord(holder) : null;
|
|
157
|
+
}
|
|
158
|
+
function acquireBuildLock({ platform, key, root = null, logFile = null }) {
|
|
159
|
+
const path = buildLockPath(platform, key);
|
|
160
|
+
const attempt = (0, ownership_claim_exports.tryAcquireClaim)({
|
|
161
|
+
root: path,
|
|
162
|
+
mode: "exclusive",
|
|
163
|
+
label: label(platform),
|
|
164
|
+
details: {
|
|
165
|
+
projectRoot: root,
|
|
166
|
+
logFile
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
const reaped = attempt.reaped[0];
|
|
170
|
+
if (attempt.acquired) {
|
|
171
|
+
const handle = {
|
|
172
|
+
acquired: true,
|
|
173
|
+
path,
|
|
174
|
+
lock: {
|
|
175
|
+
pid: attempt.acquired.owner.pid,
|
|
176
|
+
projectRoot: root,
|
|
177
|
+
startedAt: attempt.acquired.startedAt,
|
|
178
|
+
logFile
|
|
179
|
+
},
|
|
180
|
+
claim: attempt.acquired
|
|
181
|
+
};
|
|
182
|
+
declareSpawnsOn(ambientContext(), attempt.acquired);
|
|
183
|
+
return reaped ? {
|
|
184
|
+
...handle,
|
|
185
|
+
tookOver: toRecord(reaped)
|
|
186
|
+
} : handle;
|
|
187
|
+
}
|
|
188
|
+
if (attempt.pending) (0, ownership_claim_exports.releaseClaim)(attempt.pending);
|
|
189
|
+
const holder = attempt.held ?? attempt.waitingFor?.[0];
|
|
190
|
+
return holder ? {
|
|
191
|
+
held: toRecord(holder),
|
|
192
|
+
path
|
|
193
|
+
} : { path };
|
|
194
|
+
}
|
|
195
|
+
function releaseBuildLock(handle) {
|
|
196
|
+
stopDeclaringSpawnsOn(ambientContext(), handle?.claim);
|
|
197
|
+
return (0, ownership_claim_exports.releaseClaim)(handle?.claim);
|
|
198
|
+
}
|
|
199
|
+
function listBuildLocks() {
|
|
200
|
+
const dir = buildLocksDir();
|
|
201
|
+
let names;
|
|
202
|
+
try {
|
|
203
|
+
names = readdirSync(dir);
|
|
204
|
+
} catch {
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
const locks = [];
|
|
208
|
+
for (const name of names) {
|
|
209
|
+
if (!name.endsWith(LOCK_SUFFIX$1)) continue;
|
|
210
|
+
const path = join(dir, name);
|
|
211
|
+
try {
|
|
212
|
+
if (!statSync(path).isDirectory()) continue;
|
|
213
|
+
} catch {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const stem = name.slice(0, -5);
|
|
217
|
+
const cut = stem.indexOf("-");
|
|
218
|
+
const survey = (0, ownership_claim_exports.readClaimSet)(path);
|
|
219
|
+
const holder = survey.live[0] ?? survey.dead[0];
|
|
220
|
+
const info = holder ? toRecord(holder) : null;
|
|
221
|
+
locks.push({
|
|
222
|
+
path,
|
|
223
|
+
name,
|
|
224
|
+
platform: cut > 0 ? stem.slice(0, cut) : stem,
|
|
225
|
+
key: cut > 0 ? stem.slice(cut + 1) : null,
|
|
226
|
+
pid: info?.pid ?? null,
|
|
227
|
+
projectRoot: info?.projectRoot ?? null,
|
|
228
|
+
startedAt: info?.startedAt ?? null,
|
|
229
|
+
logFile: info?.logFile ?? null,
|
|
230
|
+
alive: survey.live.length > 0,
|
|
231
|
+
unresolved: survey.unresolved.length > 0
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return locks;
|
|
235
|
+
}
|
|
236
|
+
function waitingLine({ projectRoot, pid, elapsedMs, logFile }) {
|
|
237
|
+
const where = projectRoot || "another workspace";
|
|
238
|
+
const tail = logFile ? ` -- tail ${logFile}` : "";
|
|
239
|
+
return `${"build".padEnd(11)} waiting on ${where} (pid ${pid ?? "?"}, ${formatElapsed(elapsedMs)} elapsed)${tail} -- stim guide lifecycle concurrency`;
|
|
240
|
+
}
|
|
241
|
+
function takeoverLine({ projectRoot, pid, logFile, startedAt = null, now = Date.now }) {
|
|
242
|
+
const where = projectRoot || "another workspace";
|
|
243
|
+
const started = startedAt ? Date.parse(startedAt) : NaN;
|
|
244
|
+
const ago = Number.isFinite(started) ? ` (it started ${formatElapsed(now() - started)} ago)` : "";
|
|
245
|
+
const tail = logFile ? ` -- read ${logFile} before this one finishes` : "";
|
|
246
|
+
return `RETRY: ${where}'s build of this fingerprint (pid ${pid ?? "?"}) FAILED without an artifact${ago}, and this run rebuilds the SAME inputs, so expect the same failure unless something changed${tail}`;
|
|
247
|
+
}
|
|
248
|
+
async function waitForBuild({ platform, key, resolve = resolveBuild, intervalMs = WAIT_POLL_MS, progressMs = WAIT_PROGRESS_MS, ceilingMs = WAIT_CEILING_MS, out = () => {}, now = Date.now, sleep = sleepAsync }) {
|
|
249
|
+
const path = buildLockPath(platform, key);
|
|
250
|
+
const started = now();
|
|
251
|
+
let lastProgress = started;
|
|
252
|
+
let holder = readBuildLock(path);
|
|
253
|
+
for (;;) {
|
|
254
|
+
const hit = resolve(platform, key);
|
|
255
|
+
if (hit) return {
|
|
256
|
+
hit,
|
|
257
|
+
waitedMs: now() - started
|
|
258
|
+
};
|
|
259
|
+
const survey = (0, ownership_claim_exports.readClaimSet)(path);
|
|
260
|
+
const unresolved = survey.unresolved[0];
|
|
261
|
+
if (unresolved) throw new ownership_claim_exports.ClaimRefusedError({
|
|
262
|
+
claimPath: unresolved.path,
|
|
263
|
+
root: path,
|
|
264
|
+
label: label(platform),
|
|
265
|
+
reason: unresolved.reason
|
|
266
|
+
});
|
|
267
|
+
const live = survey.live[0];
|
|
268
|
+
if (live) holder = toRecord(live);
|
|
269
|
+
else {
|
|
270
|
+
const dead = survey.dead[0];
|
|
271
|
+
if (dead) return {
|
|
272
|
+
builderFailed: `the builder (pid ${dead.owner.pid}) is gone`,
|
|
273
|
+
holder: toRecord(dead),
|
|
274
|
+
waitedMs: now() - started
|
|
275
|
+
};
|
|
276
|
+
return {
|
|
277
|
+
lockReleased: true,
|
|
278
|
+
holder,
|
|
279
|
+
waitedMs: now() - started
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
const elapsed = now() - started;
|
|
283
|
+
if (elapsed >= ceilingMs) {
|
|
284
|
+
const err = /* @__PURE__ */ new Error(`Waited ${formatElapsed(elapsed)} for ${holder?.projectRoot || "another workspace"}'s ${platform} build of ${key} without an artifact, and pid ${holder?.pid} is still the live holder. The lock is ${path}; remove it if that process is not really building:\n ${(0, ownership_claim_exports.claimRemoveCommand)(path)}`);
|
|
285
|
+
err.code = "STIM_BUILD_WAIT_TIMEOUT";
|
|
286
|
+
err.lockPath = path;
|
|
287
|
+
err.holder = holder ?? void 0;
|
|
288
|
+
throw err;
|
|
289
|
+
}
|
|
290
|
+
if (now() - lastProgress >= progressMs) {
|
|
291
|
+
lastProgress = now();
|
|
292
|
+
out(waitingLine({
|
|
293
|
+
projectRoot: holder.projectRoot,
|
|
294
|
+
pid: holder.pid,
|
|
295
|
+
elapsedMs: elapsed,
|
|
296
|
+
logFile: holder.logFile
|
|
297
|
+
}));
|
|
298
|
+
}
|
|
299
|
+
await sleep(intervalMs);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
async function waitForSharedBuild({ platform, key, fingerprint, root, logFile, command, acquire, wait, now, phase, waitingOn, warn, out }) {
|
|
303
|
+
const waitStarted = now();
|
|
304
|
+
let failedHolder;
|
|
305
|
+
let released = null;
|
|
306
|
+
for (;;) {
|
|
307
|
+
let attempt = null;
|
|
308
|
+
try {
|
|
309
|
+
attempt = acquire({
|
|
310
|
+
platform,
|
|
311
|
+
key,
|
|
312
|
+
root,
|
|
313
|
+
logFile
|
|
314
|
+
});
|
|
315
|
+
} catch (err) {
|
|
316
|
+
const refusal = (0, ownership_claim_exports.claimFailure)(err, command);
|
|
317
|
+
if (refusal) return { refusal };
|
|
318
|
+
warn(`could not take the build lock: ${err?.message || err}; building anyway`);
|
|
319
|
+
}
|
|
320
|
+
if (attempt?.acquired) {
|
|
321
|
+
const previous = attempt.tookOver ?? failedHolder;
|
|
322
|
+
if (previous) warn(takeoverLine(previous));
|
|
323
|
+
waitingOn(null);
|
|
324
|
+
return {
|
|
325
|
+
refusal: null,
|
|
326
|
+
lock: attempt,
|
|
327
|
+
hit: null,
|
|
328
|
+
released
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
if (!attempt?.held) {
|
|
332
|
+
waitingOn(null);
|
|
333
|
+
return {
|
|
334
|
+
refusal: null,
|
|
335
|
+
lock: null,
|
|
336
|
+
hit: null,
|
|
337
|
+
released
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
released = null;
|
|
341
|
+
const holder = attempt.held;
|
|
342
|
+
const who = holder.projectRoot || "another workspace";
|
|
343
|
+
phase(`${who} is already building ${shortHash(fingerprint)} (pid ${holder.pid})${holder.logFile ? ` -- tail ${holder.logFile}` : ""} -- stim guide lifecycle concurrency`);
|
|
344
|
+
waitingOn(holder.projectRoot || null);
|
|
345
|
+
let waited;
|
|
346
|
+
try {
|
|
347
|
+
const ceilingMs = WAIT_CEILING_MS - (now() - waitStarted);
|
|
348
|
+
if (ceilingMs <= 0) throw Object.assign(/* @__PURE__ */ new Error(`Waited ${formatDuration(now() - waitStarted)} for shared builds without an artifact; ${who} (pid ${holder.pid}) holds ${attempt.path}.`), {
|
|
349
|
+
code: "STIM_BUILD_WAIT_TIMEOUT",
|
|
350
|
+
lockPath: attempt.path
|
|
351
|
+
});
|
|
352
|
+
waited = await wait({
|
|
353
|
+
platform,
|
|
354
|
+
key,
|
|
355
|
+
out,
|
|
356
|
+
ceilingMs
|
|
357
|
+
});
|
|
358
|
+
} catch (err) {
|
|
359
|
+
const refusal = (0, ownership_claim_exports.claimFailure)(err, command);
|
|
360
|
+
if (refusal) return { refusal };
|
|
361
|
+
const timeout = err;
|
|
362
|
+
if (timeout?.code !== "STIM_BUILD_WAIT_TIMEOUT") throw err;
|
|
363
|
+
return { refusal: {
|
|
364
|
+
code: "STIM_BUILD_WAIT_TIMEOUT",
|
|
365
|
+
message: timeout.message,
|
|
366
|
+
remedy: `Check pid ${holder.pid}; if it is not really building, remove ${timeout.lockPath} and run \`${command}\` again.`
|
|
367
|
+
} };
|
|
368
|
+
}
|
|
369
|
+
if (waited.hit) {
|
|
370
|
+
waitingOn(null);
|
|
371
|
+
phase(`waited ${formatDuration(waited.waitedMs)} for ${who}'s build -> installed from cache -- stim guide lifecycle concurrency`);
|
|
372
|
+
return {
|
|
373
|
+
refusal: null,
|
|
374
|
+
lock: null,
|
|
375
|
+
hit: {
|
|
376
|
+
path: waited.hit,
|
|
377
|
+
waited: {
|
|
378
|
+
pid: holder.pid,
|
|
379
|
+
ms: waited.waitedMs
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
released
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
if (waited.lockReleased) {
|
|
386
|
+
failedHolder = void 0;
|
|
387
|
+
released = {
|
|
388
|
+
facts: {
|
|
389
|
+
pid: holder.pid,
|
|
390
|
+
ms: waited.waitedMs
|
|
391
|
+
},
|
|
392
|
+
who
|
|
393
|
+
};
|
|
394
|
+
phase(`${who}'s build lock was released; rechecking this workspace before building`);
|
|
395
|
+
} else {
|
|
396
|
+
warn(`${who}'s build ended without an artifact (${waited.builderFailed}); retrying the build lock`);
|
|
397
|
+
failedHolder = holder;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const DEFAULT_LEASE_MS = 3e5;
|
|
402
|
+
const LEASE_SUFFIX = ".json";
|
|
403
|
+
const LOCK_SUFFIX = ".lock";
|
|
404
|
+
const DURATION = /^[1-9][0-9]*(s|m)$/;
|
|
405
|
+
function deviceLocksDir() {
|
|
406
|
+
return join(getConfigDir(), "device-locks");
|
|
407
|
+
}
|
|
408
|
+
function leaseStem(platform, id) {
|
|
409
|
+
return `${segment(platform)}-${segment(id)}`;
|
|
410
|
+
}
|
|
411
|
+
function deviceLeasePath(platform, id) {
|
|
412
|
+
return join(deviceLocksDir(), `${leaseStem(platform, id)}${LEASE_SUFFIX}`);
|
|
413
|
+
}
|
|
414
|
+
function deviceLeaseLockPath(platform, id) {
|
|
415
|
+
return join(deviceLocksDir(), `${leaseStem(platform, id)}${LOCK_SUFFIX}`);
|
|
416
|
+
}
|
|
417
|
+
const fileLeaseIo = {
|
|
418
|
+
withHolderLock: withWorkspaceStateLock,
|
|
419
|
+
now: () => Date.now(),
|
|
420
|
+
readLease(path) {
|
|
421
|
+
try {
|
|
422
|
+
return readFileSync(path, "utf-8");
|
|
423
|
+
} catch {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
writeLease(path, body) {
|
|
428
|
+
mkdirSync(deviceLocksDir(), { recursive: true });
|
|
429
|
+
const tmp = `${path}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
430
|
+
writeFileSync(tmp, body);
|
|
431
|
+
try {
|
|
432
|
+
renameSync(tmp, path);
|
|
433
|
+
} catch (error) {
|
|
434
|
+
rmSync(tmp, { force: true });
|
|
435
|
+
throw error;
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
removeLease(path) {
|
|
439
|
+
rmSync(path, { force: true });
|
|
440
|
+
},
|
|
441
|
+
listLeaseNames() {
|
|
442
|
+
try {
|
|
443
|
+
return readdirSync(deviceLocksDir());
|
|
444
|
+
} catch {
|
|
445
|
+
return [];
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
withLeaseLock(lockPath, fn) {
|
|
449
|
+
return withDirLock$1(lockPath, fn, { ensureParent: () => mkdirSync(deviceLocksDir(), { recursive: true }) });
|
|
450
|
+
},
|
|
451
|
+
readHolder(root) {
|
|
452
|
+
return parseWorkspaceLeases(readWorkspaceState$1(root)?.deviceLeases);
|
|
453
|
+
},
|
|
454
|
+
writeHolder(root, leases) {
|
|
455
|
+
if (Object.keys(leases).length === 0) clearWorkspaceStateKeys(root, ["deviceLeases"]);
|
|
456
|
+
else writeWorkspaceState(root, { deviceLeases: leases });
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
function text(value) {
|
|
460
|
+
return typeof value === "string" && value.trim() !== "" ? value : null;
|
|
461
|
+
}
|
|
462
|
+
function isPlatform(value) {
|
|
463
|
+
return value === "ios" || value === "android";
|
|
464
|
+
}
|
|
465
|
+
function parseLease(raw) {
|
|
466
|
+
if (typeof raw !== "string") return null;
|
|
467
|
+
let parsed;
|
|
468
|
+
try {
|
|
469
|
+
parsed = JSON.parse(raw);
|
|
470
|
+
} catch {
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
if (!isJsonObject(parsed)) return null;
|
|
474
|
+
const entry = parsed;
|
|
475
|
+
if (entry.version !== 1 || !isPlatform(entry.platform)) return null;
|
|
476
|
+
const id = text(entry.id);
|
|
477
|
+
const holder = text(entry.holder);
|
|
478
|
+
const token = text(entry.token);
|
|
479
|
+
const expiresAt = text(entry.expiresAt);
|
|
480
|
+
if (!id || !holder || !token || !expiresAt || !Number.isFinite(Date.parse(expiresAt))) return null;
|
|
481
|
+
if (entry.slot !== void 0) {
|
|
482
|
+
if (typeof entry.slot !== "string") return null;
|
|
483
|
+
try {
|
|
484
|
+
validateDeviceSlot(entry.slot);
|
|
485
|
+
} catch {
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return {
|
|
490
|
+
version: 1,
|
|
491
|
+
...typeof entry.slot === "string" ? { slot: entry.slot } : {},
|
|
492
|
+
platform: entry.platform,
|
|
493
|
+
id,
|
|
494
|
+
deviceName: text(entry.deviceName),
|
|
495
|
+
holder,
|
|
496
|
+
token,
|
|
497
|
+
grantedAt: text(entry.grantedAt),
|
|
498
|
+
expiresAt
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function parseWorkspaceLeases(value) {
|
|
502
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
503
|
+
const leases = {};
|
|
504
|
+
for (const [platform, entry] of Object.entries(value)) {
|
|
505
|
+
if (!parseDeviceSlotKey(platform) || !entry || typeof entry !== "object") continue;
|
|
506
|
+
const record = entry;
|
|
507
|
+
const id = text(record.id);
|
|
508
|
+
const token = text(record.token);
|
|
509
|
+
if (!id || !token) continue;
|
|
510
|
+
leases[platform] = {
|
|
511
|
+
id,
|
|
512
|
+
token,
|
|
513
|
+
kind: record.kind === "run" ? "run" : "declared"
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
return leases;
|
|
517
|
+
}
|
|
518
|
+
function leaseIsExpired(lease, now) {
|
|
519
|
+
return Date.parse(lease.expiresAt) <= now;
|
|
520
|
+
}
|
|
521
|
+
function parseLeaseDuration(value) {
|
|
522
|
+
const raw = String(value ?? "").trim();
|
|
523
|
+
const match = DURATION.exec(raw);
|
|
524
|
+
if (!match) return { error: `--for must be a whole number of seconds or minutes, such as 90s or 5m (got "${raw}").` };
|
|
525
|
+
const ms = Number(raw.slice(0, -1)) * (match[1] === "s" ? 1e3 : 6e4);
|
|
526
|
+
if (ms < 1e4 || ms > 18e5) return { error: `--for must be between 10s and 30m (got "${raw}").` };
|
|
527
|
+
return { ms };
|
|
528
|
+
}
|
|
529
|
+
function listLeaseFiles(io = fileLeaseIo) {
|
|
530
|
+
const dir = deviceLocksDir();
|
|
531
|
+
const entries = [];
|
|
532
|
+
for (const name of io.listLeaseNames().toSorted()) {
|
|
533
|
+
if (!name.endsWith(LEASE_SUFFIX)) continue;
|
|
534
|
+
const path = join(dir, name);
|
|
535
|
+
const lease = parseLease(io.readLease(path));
|
|
536
|
+
const stem = name.slice(0, -5);
|
|
537
|
+
const cut = stem.indexOf("-");
|
|
538
|
+
entries.push({
|
|
539
|
+
path,
|
|
540
|
+
name,
|
|
541
|
+
platform: lease?.platform ?? (cut > 0 ? stem.slice(0, cut) : stem),
|
|
542
|
+
id: lease?.id ?? (cut > 0 ? stem.slice(cut + 1) : null),
|
|
543
|
+
lease
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
return entries;
|
|
547
|
+
}
|
|
548
|
+
function writeLease(lease, io) {
|
|
549
|
+
io.writeLease(deviceLeasePath(lease.platform, lease.id), `${JSON.stringify(lease, null, 2)}\n`);
|
|
550
|
+
return lease;
|
|
551
|
+
}
|
|
552
|
+
function takeLease({ root, platform, slot = "default", id, deviceName = null, kind, durationMs = DEFAULT_LEASE_MS }, io = fileLeaseIo) {
|
|
553
|
+
return withHolderLock(io, root, () => {
|
|
554
|
+
const key = deviceSlotKey(platform, slot);
|
|
555
|
+
const previous = io.readHolder(root)[key];
|
|
556
|
+
if (previous && previous.id !== id) releaseHeld(root, platform, previous, io, slot);
|
|
557
|
+
for (const entry of listLeaseFiles(io)) {
|
|
558
|
+
const other = entry.lease;
|
|
559
|
+
if (!other || other.platform !== platform || other.holder !== root || (other.slot ?? "default") !== slot || other.id === id) continue;
|
|
560
|
+
releaseByRoot(root, platform, other.id, io);
|
|
561
|
+
}
|
|
562
|
+
const declared = previous?.kind === "declared" && previous.id === id;
|
|
563
|
+
const path = deviceLeasePath(platform, id);
|
|
564
|
+
const result = io.withLeaseLock(deviceLeaseLockPath(platform, id), () => {
|
|
565
|
+
const now = io.now();
|
|
566
|
+
const raw = io.readLease(path);
|
|
567
|
+
const current = parseLease(raw);
|
|
568
|
+
if (raw !== null && !current) return {
|
|
569
|
+
status: "unreadable",
|
|
570
|
+
path
|
|
571
|
+
};
|
|
572
|
+
const asked = now + durationMs;
|
|
573
|
+
if (current && !leaseIsExpired(current, now)) {
|
|
574
|
+
if (current.holder !== root || (current.slot ?? "default") !== slot) return {
|
|
575
|
+
status: "held",
|
|
576
|
+
lease: current
|
|
577
|
+
};
|
|
578
|
+
const floor = declared && kind === "run" ? Math.max(asked, Date.parse(current.expiresAt)) : asked;
|
|
579
|
+
return {
|
|
580
|
+
status: "set",
|
|
581
|
+
lease: writeLease({
|
|
582
|
+
...current,
|
|
583
|
+
deviceName: deviceName ?? current.deviceName,
|
|
584
|
+
expiresAt: new Date(floor).toISOString()
|
|
585
|
+
}, io)
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
const expiresAt = new Date(asked).toISOString();
|
|
589
|
+
return {
|
|
590
|
+
status: "taken",
|
|
591
|
+
lease: writeLease({
|
|
592
|
+
version: 1,
|
|
593
|
+
...slot === "default" ? {} : { slot },
|
|
594
|
+
platform,
|
|
595
|
+
id,
|
|
596
|
+
deviceName,
|
|
597
|
+
holder: root,
|
|
598
|
+
token: randomBytes(12).toString("hex"),
|
|
599
|
+
grantedAt: new Date(now).toISOString(),
|
|
600
|
+
expiresAt
|
|
601
|
+
}, io)
|
|
602
|
+
};
|
|
603
|
+
});
|
|
604
|
+
if (result.status === "taken" || result.status === "set") {
|
|
605
|
+
const recorded = result.status === "set" && declared ? "declared" : kind;
|
|
606
|
+
io.writeHolder(root, {
|
|
607
|
+
...io.readHolder(root),
|
|
608
|
+
[key]: {
|
|
609
|
+
id,
|
|
610
|
+
token: result.lease.token,
|
|
611
|
+
kind: recorded
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
return result;
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
function raiseLease({ root, platform, slot = "default", minMs }, io = fileLeaseIo) {
|
|
619
|
+
const record = io.readHolder(root)[deviceSlotKey(platform, slot)];
|
|
620
|
+
if (!record) return { status: "none" };
|
|
621
|
+
const path = deviceLeasePath(platform, record.id);
|
|
622
|
+
return io.withLeaseLock(deviceLeaseLockPath(platform, record.id), () => {
|
|
623
|
+
const current = parseLease(io.readLease(path));
|
|
624
|
+
if (!current || current.token !== record.token) return {
|
|
625
|
+
status: "lost",
|
|
626
|
+
lease: current
|
|
627
|
+
};
|
|
628
|
+
const floor = io.now() + minMs;
|
|
629
|
+
if (Date.parse(current.expiresAt) >= floor) return {
|
|
630
|
+
status: "raised",
|
|
631
|
+
lease: current
|
|
632
|
+
};
|
|
633
|
+
return {
|
|
634
|
+
status: "raised",
|
|
635
|
+
lease: writeLease({
|
|
636
|
+
...current,
|
|
637
|
+
expiresAt: new Date(floor).toISOString()
|
|
638
|
+
}, io)
|
|
639
|
+
};
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
function summarize(lease) {
|
|
643
|
+
return {
|
|
644
|
+
platform: lease.platform,
|
|
645
|
+
id: lease.id,
|
|
646
|
+
deviceName: lease.deviceName,
|
|
647
|
+
expiresAt: lease.expiresAt
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
function releaseHeld(root, platform, record, io, slot = "default") {
|
|
651
|
+
const key = deviceSlotKey(platform, slot);
|
|
652
|
+
const path = deviceLeasePath(platform, record.id);
|
|
653
|
+
const released = io.withLeaseLock(deviceLeaseLockPath(platform, record.id), () => {
|
|
654
|
+
const current = parseLease(io.readLease(path));
|
|
655
|
+
if (!current || current.token !== record.token) return null;
|
|
656
|
+
io.removeLease(path);
|
|
657
|
+
return current;
|
|
658
|
+
});
|
|
659
|
+
const held = io.readHolder(root);
|
|
660
|
+
if (held[key]?.token === record.token) {
|
|
661
|
+
delete held[key];
|
|
662
|
+
io.writeHolder(root, held);
|
|
663
|
+
}
|
|
664
|
+
return released;
|
|
665
|
+
}
|
|
666
|
+
function releaseByRoot(root, platform, id, io) {
|
|
667
|
+
const path = deviceLeasePath(platform, id);
|
|
668
|
+
return io.withLeaseLock(deviceLeaseLockPath(platform, id), () => {
|
|
669
|
+
const current = parseLease(io.readLease(path));
|
|
670
|
+
if (!current || current.holder !== root) return null;
|
|
671
|
+
io.removeLease(path);
|
|
672
|
+
return current;
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
function releaseRunLease({ root, platform, slot = "default" }, io = fileLeaseIo) {
|
|
676
|
+
return withHolderLock(io, root, () => {
|
|
677
|
+
const record = io.readHolder(root)[deviceSlotKey(platform, slot)];
|
|
678
|
+
if (!record || record.kind !== "run") return null;
|
|
679
|
+
const released = releaseHeld(root, platform, record, io, slot);
|
|
680
|
+
return released ? summarize(released) : null;
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
function releaseWorkspaceLeases(root, { platform = null, slot = null } = {}, io = fileLeaseIo) {
|
|
684
|
+
return withHolderLock(io, root, () => {
|
|
685
|
+
const released = [];
|
|
686
|
+
for (const [name, record] of Object.entries(io.readHolder(root))) {
|
|
687
|
+
const parsed = parseDeviceSlotKey(name);
|
|
688
|
+
if (!parsed || platform && parsed.platform !== platform || slot !== null && parsed.slot !== slot) continue;
|
|
689
|
+
const lease = releaseHeld(root, parsed.platform, record, io, parsed.slot);
|
|
690
|
+
if (lease) released.push(summarize(lease));
|
|
691
|
+
}
|
|
692
|
+
for (const entry of listLeaseFiles(io)) {
|
|
693
|
+
const lease = entry.lease;
|
|
694
|
+
if (!lease || lease.holder !== root) continue;
|
|
695
|
+
if (platform && lease.platform !== platform) continue;
|
|
696
|
+
if (slot !== null && (lease.slot ?? "default") !== slot) continue;
|
|
697
|
+
const byRoot = releaseByRoot(root, lease.platform, lease.id, io);
|
|
698
|
+
if (byRoot) released.push(summarize(byRoot));
|
|
699
|
+
}
|
|
700
|
+
return released;
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
/** Releases workspace leases with asynchronous lock waits and the same ownership checks. */
|
|
704
|
+
async function releaseWorkspaceLeasesAsync(root, options = {}) {
|
|
705
|
+
const released = [];
|
|
706
|
+
const lockOptions = { ensureParent: () => mkdirSync(deviceLocksDir(), { recursive: true }) };
|
|
707
|
+
for (;;) {
|
|
708
|
+
const attempt = {};
|
|
709
|
+
const io = {
|
|
710
|
+
...fileLeaseIo,
|
|
711
|
+
removeLease(path) {
|
|
712
|
+
const lease = parseLease(fileLeaseIo.readLease(path));
|
|
713
|
+
fileLeaseIo.removeLease(path);
|
|
714
|
+
if (lease) released.push(summarize(lease));
|
|
715
|
+
},
|
|
716
|
+
withLeaseLock(path, fn) {
|
|
717
|
+
try {
|
|
718
|
+
return withDirLock$1(path, fn, {
|
|
719
|
+
...lockOptions,
|
|
720
|
+
waitMs: 0
|
|
721
|
+
});
|
|
722
|
+
} catch (error) {
|
|
723
|
+
if (error instanceof Error && "code" in error && error.code === "STIM_LOCK_TIMEOUT") attempt.blocked = {
|
|
724
|
+
path,
|
|
725
|
+
error
|
|
726
|
+
};
|
|
727
|
+
throw error;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
await withWorkspaceStateLockAsync(root, () => {
|
|
732
|
+
try {
|
|
733
|
+
releaseWorkspaceLeases(root, options, io);
|
|
734
|
+
} catch (error) {
|
|
735
|
+
if (error !== attempt.blocked?.error) throw error;
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
const blocked = attempt.blocked;
|
|
739
|
+
if (!blocked) return released;
|
|
740
|
+
await withDirLockAsync$1(blocked.path, () => void 0, lockOptions);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
function removeExpiredLease(entry, io = fileLeaseIo) {
|
|
744
|
+
const lease = entry.lease;
|
|
745
|
+
if (!lease) return false;
|
|
746
|
+
return io.withLeaseLock(deviceLeaseLockPath(lease.platform, lease.id), () => {
|
|
747
|
+
const current = parseLease(io.readLease(entry.path));
|
|
748
|
+
if (!current || !leaseIsExpired(current, io.now())) return false;
|
|
749
|
+
io.removeLease(entry.path);
|
|
750
|
+
return true;
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
function selectPoolDevice({ candidates, leases, held = null, now }) {
|
|
754
|
+
const ordered = candidates.toSorted((a, b) => {
|
|
755
|
+
const left = a.id.toLowerCase();
|
|
756
|
+
const right = b.id.toLowerCase();
|
|
757
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
758
|
+
});
|
|
759
|
+
if (held) {
|
|
760
|
+
const mine = ordered.find((candidate) => candidate.id === held);
|
|
761
|
+
return mine ? {
|
|
762
|
+
status: "selected",
|
|
763
|
+
candidate: mine
|
|
764
|
+
} : {
|
|
765
|
+
status: "held-disconnected",
|
|
766
|
+
id: held
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
const preferred = ordered.filter((candidate) => !candidate.fallback);
|
|
770
|
+
const pool = preferred.length > 0 ? preferred : ordered;
|
|
771
|
+
if (pool.length === 0) return { status: "none" };
|
|
772
|
+
const byId = new Map(leases.map((lease) => [lease.id, lease]));
|
|
773
|
+
const free = (candidate) => {
|
|
774
|
+
const lease = byId.get(candidate.id);
|
|
775
|
+
return !lease || leaseIsExpired(lease, now);
|
|
776
|
+
};
|
|
777
|
+
const holders = [];
|
|
778
|
+
for (const candidate of pool) {
|
|
779
|
+
if (free(candidate)) return {
|
|
780
|
+
status: "selected",
|
|
781
|
+
candidate
|
|
782
|
+
};
|
|
783
|
+
const lease = byId.get(candidate.id);
|
|
784
|
+
holders.push({
|
|
785
|
+
id: lease.id,
|
|
786
|
+
holder: lease.holder,
|
|
787
|
+
expiresAt: lease.expiresAt
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
return {
|
|
791
|
+
status: "busy",
|
|
792
|
+
holders,
|
|
793
|
+
spare: ordered.filter((candidate) => candidate.fallback && free(candidate)).map((candidate) => candidate.id)
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
function withHolderLock(io, root, fn) {
|
|
797
|
+
return io.withHolderLock ? io.withHolderLock(root, fn) : fn();
|
|
798
|
+
}
|
|
799
|
+
//#endregion
|
|
800
|
+
//#region src/devices/activity.ts
|
|
801
|
+
const ACTIVE_WINDOW_MS = 6e5;
|
|
802
|
+
const LOG_TAIL_BYTES = 262144;
|
|
803
|
+
function classifyActivity(evidence, now) {
|
|
804
|
+
const last = evidence.recency.reduce((best, entry) => Number.isFinite(entry.at) && (!best || entry.at > best.at) ? entry : best, null);
|
|
805
|
+
const recent = {};
|
|
806
|
+
for (const { basis, at } of evidence.recency) if (Number.isFinite(at) && !(Date.parse(recent[basis] ?? "") >= at)) recent[basis] = new Date(at).toISOString();
|
|
807
|
+
const lastActivityAt = last ? {
|
|
808
|
+
lastActivityAt: new Date(last.at).toISOString(),
|
|
809
|
+
recent
|
|
810
|
+
} : {};
|
|
811
|
+
const [first] = evidence.drivers;
|
|
812
|
+
if (first) {
|
|
813
|
+
const { basis: _basis, ...driver } = first;
|
|
814
|
+
return {
|
|
815
|
+
state: "driven",
|
|
816
|
+
driver,
|
|
817
|
+
...lastActivityAt,
|
|
818
|
+
basis: [...new Set(evidence.drivers.map((entry) => entry.basis))]
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
if (evidence.unknown.length) return {
|
|
822
|
+
state: "unknown",
|
|
823
|
+
...lastActivityAt,
|
|
824
|
+
basis: [...new Set(evidence.unknown)]
|
|
825
|
+
};
|
|
826
|
+
return {
|
|
827
|
+
state: Boolean(last && now - last.at < ACTIVE_WINDOW_MS) ? "active" : "idle",
|
|
828
|
+
...lastActivityAt,
|
|
829
|
+
basis: [...new Set(evidence.recency.map((entry) => entry.basis))]
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
function idleForMs(activity, now) {
|
|
833
|
+
if (activity?.state !== "idle" || !activity.lastActivityAt) return null;
|
|
834
|
+
const at = Date.parse(activity.lastActivityAt);
|
|
835
|
+
return Number.isFinite(at) ? Math.max(0, now - at) : null;
|
|
836
|
+
}
|
|
837
|
+
function field(entry, pidKey, startKey) {
|
|
838
|
+
const pid = entry[pidKey];
|
|
839
|
+
const startTime = entry[startKey];
|
|
840
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || typeof startTime !== "string") return null;
|
|
841
|
+
return {
|
|
842
|
+
pid,
|
|
843
|
+
startTime
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
function parseObject(raw) {
|
|
847
|
+
if (raw === null) return null;
|
|
848
|
+
try {
|
|
849
|
+
const parsed = JSON.parse(raw);
|
|
850
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
851
|
+
} catch {
|
|
852
|
+
return null;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
function parseAgentDeviceRecord(kind, path, raw) {
|
|
856
|
+
const entry = parseObject(raw);
|
|
857
|
+
const stem = basename(path, ".json");
|
|
858
|
+
const device = entry?.device && typeof entry.device === "object" ? entry.device : null;
|
|
859
|
+
const recordedId = kind === "claim" ? device?.id : entry?.deviceId;
|
|
860
|
+
const deviceId = typeof recordedId === "string" && recordedId ? recordedId : kind === "runner-lease" ? stem : null;
|
|
861
|
+
const createdAtMs = typeof entry?.createdAtMs === "number" ? entry.createdAtMs : null;
|
|
862
|
+
return {
|
|
863
|
+
path,
|
|
864
|
+
kind,
|
|
865
|
+
deviceId,
|
|
866
|
+
session: kind === "claim" && typeof entry?.session === "string" && entry.session ? entry.session : null,
|
|
867
|
+
workspace: kind === "claim" && typeof entry?.workspace === "string" && entry.workspace ? entry.workspace : null,
|
|
868
|
+
stateDir: kind === "claim" && typeof entry?.stateDir === "string" && entry.stateDir ? entry.stateDir : null,
|
|
869
|
+
deviceName: kind === "claim" && typeof device?.name === "string" ? device.name : null,
|
|
870
|
+
readable: Boolean(entry && deviceId),
|
|
871
|
+
owner: entry ? field(entry, "ownerPid", "ownerStartTime") : null,
|
|
872
|
+
runner: entry && kind === "runner-lease" ? field(entry, "runnerPid", "runnerStartTime") : null,
|
|
873
|
+
claimProtocol: kind === "runner-lease" && entry?.deviceClaimProtocol === 1,
|
|
874
|
+
createdAtMs,
|
|
875
|
+
xctestrunPath: typeof entry?.xctestrunPath === "string" ? entry.xctestrunPath : null
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
function processMatches(entry, startOf) {
|
|
879
|
+
const start = startOf(entry.pid);
|
|
880
|
+
if (start.status === "gone") return "dead";
|
|
881
|
+
if (start.status === "unknown") return "unknown";
|
|
882
|
+
const recorded = Date.parse(entry.startTime);
|
|
883
|
+
if (!Number.isFinite(recorded)) return "unknown";
|
|
884
|
+
return Math.floor(start.startedAtMs / 1e3) === Math.floor(recorded / 1e3) ? "live" : "dead";
|
|
885
|
+
}
|
|
886
|
+
function agentDeviceDaemon(stateDir, startOf = process_identity_exports.inspectProcessStart) {
|
|
887
|
+
let raw;
|
|
888
|
+
try {
|
|
889
|
+
raw = readFileSync(join(stateDir, "daemon.json"), "utf8");
|
|
890
|
+
} catch (error) {
|
|
891
|
+
return error.code === "ENOENT" ? "none" : "unknown";
|
|
892
|
+
}
|
|
893
|
+
const entry = parseObject(raw);
|
|
894
|
+
const owner = entry && field(entry, "pid", "processStartTime");
|
|
895
|
+
return owner ? processMatches(owner, startOf) : "unknown";
|
|
896
|
+
}
|
|
897
|
+
function agentDeviceLiveness(record, startOf) {
|
|
898
|
+
const results = (record.kind === "claim" ? [record.owner] : [record.owner, record.runner]).map((entry) => entry ? processMatches(entry, startOf) : "unknown");
|
|
899
|
+
if (results.includes("dead")) return "dead";
|
|
900
|
+
if (!record.readable || results.includes("unknown")) return "unknown";
|
|
901
|
+
return "live";
|
|
902
|
+
}
|
|
903
|
+
const HOST_PROCESS_COLUMNS = "pid=,ppid=,rss=,%cpu=,lstart=,command=";
|
|
904
|
+
function parseProcessTable(output) {
|
|
905
|
+
const rows = [];
|
|
906
|
+
for (const line of output.split("\n")) {
|
|
907
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+(?:[.,]\d+)?)\s+(\w{3}\s+\w{3}\s+\d+\s+\d{1,2}:\d{2}:\d{2}\s+\d{4})\s+(.*)$/.exec(line);
|
|
908
|
+
if (!match) continue;
|
|
909
|
+
const at = Date.parse(match[5]);
|
|
910
|
+
rows.push({
|
|
911
|
+
pid: Number(match[1]),
|
|
912
|
+
ppid: Number(match[2]),
|
|
913
|
+
rssKb: Number(match[3]),
|
|
914
|
+
cpuPercent: Number(match[4].replace(",", ".")),
|
|
915
|
+
startedAt: Number.isFinite(at) ? new Date(at).toISOString() : null,
|
|
916
|
+
command: match[6]
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
return rows;
|
|
920
|
+
}
|
|
921
|
+
function parseAndroidInstrumentation(output) {
|
|
922
|
+
const found = [];
|
|
923
|
+
for (const line of output.split("\n")) {
|
|
924
|
+
const match = /^\s*(\d+)\s+(.*)$/.exec(line);
|
|
925
|
+
if (!match) continue;
|
|
926
|
+
const tool = instrumentationTool(match[2]);
|
|
927
|
+
if (tool) found.push({
|
|
928
|
+
pid: Number(match[1]),
|
|
929
|
+
tool
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
return found;
|
|
933
|
+
}
|
|
934
|
+
function latestDeviceLogAt(lines, target) {
|
|
935
|
+
return latestRecordAt(lines, (record) => {
|
|
936
|
+
if (record.src !== "device" || record.platform !== target.platform || !isDeviceRecord(record)) return false;
|
|
937
|
+
if (target.slot === "default") return record.slot === void 0;
|
|
938
|
+
return record.slot === target.slot && (record.deviceId === void 0 || record.deviceId === target.id);
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
function latestBundleRequestAt(lines, platform) {
|
|
942
|
+
return latestRecordAt(lines, (record) => record.event === "bundle_response_started" && record.platform === platform);
|
|
943
|
+
}
|
|
944
|
+
function latestRecordAt(lines, matches) {
|
|
945
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
946
|
+
const record = parseObject(lines[i]);
|
|
947
|
+
if (record && typeof record.ts === "number" && matches(record)) return record.ts;
|
|
948
|
+
}
|
|
949
|
+
return null;
|
|
950
|
+
}
|
|
951
|
+
function isDeviceRecord(record) {
|
|
952
|
+
return !(typeof record.event === "string" && record.event.startsWith("collector_"));
|
|
953
|
+
}
|
|
954
|
+
/** The last lines of the file at `path`, at most its final 256 KiB, or null when it cannot be read. */
|
|
955
|
+
function tailLines(path) {
|
|
956
|
+
let fd;
|
|
957
|
+
try {
|
|
958
|
+
fd = openSync(path, "r");
|
|
959
|
+
const size = fstatSync(fd).size;
|
|
960
|
+
const length = Math.min(size, LOG_TAIL_BYTES);
|
|
961
|
+
const buffer = Buffer.alloc(length);
|
|
962
|
+
readSync(fd, buffer, 0, length, size - length);
|
|
963
|
+
const lines = buffer.toString("utf8").split("\n");
|
|
964
|
+
return length < size ? lines.slice(1) : lines;
|
|
965
|
+
} catch {
|
|
966
|
+
return null;
|
|
967
|
+
} finally {
|
|
968
|
+
if (fd !== void 0) closeSync(fd);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
function envDir(name, env = process.env, cwd = process.cwd()) {
|
|
972
|
+
const value = env[name]?.trim();
|
|
973
|
+
return value ? resolve(cwd, value) : null;
|
|
974
|
+
}
|
|
975
|
+
function agentDeviceDirs(home, env = process.env) {
|
|
976
|
+
const root = join(home, ".agent-device");
|
|
977
|
+
const leaseDirs = [envDir("AGENT_DEVICE_IOS_RUNNER_LEASE_DIR", env) ?? join(root, "apple-runner", "leases"), join(root, "ios-runner", "leases")];
|
|
978
|
+
return [{
|
|
979
|
+
kind: "claim",
|
|
980
|
+
dir: envDir("AGENT_DEVICE_CLAIMS_DIR", env) ?? join(root, "device-claims")
|
|
981
|
+
}, ...[...new Set(leaseDirs)].map((dir) => ({
|
|
982
|
+
kind: "runner-lease",
|
|
983
|
+
dir
|
|
984
|
+
}))];
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* When an agent-device session last recorded an event: its events file's modification time. agent-device names a
|
|
988
|
+
* session's directory after the session with every character outside `[a-zA-Z0-9._-]` replaced by `_`.
|
|
989
|
+
*/
|
|
990
|
+
function agentSessionActedAt(home, session, stateDir, env = process.env) {
|
|
991
|
+
let latest = null;
|
|
992
|
+
for (const root of stateDir ? [stateDir] : agentDeviceStateDirs(home, env)) try {
|
|
993
|
+
const at = statSync(join(root, "sessions", session.replaceAll(/[^a-zA-Z0-9._-]/g, "_"), "events.ndjson")).mtimeMs;
|
|
994
|
+
latest = latest === null ? at : Math.max(latest, at);
|
|
995
|
+
} catch {}
|
|
996
|
+
return latest;
|
|
997
|
+
}
|
|
998
|
+
function readAgentDeviceRecords(home, env = process.env) {
|
|
999
|
+
return agentDeviceDirs(home, env).flatMap(({ kind, dir }) => {
|
|
1000
|
+
let names;
|
|
1001
|
+
try {
|
|
1002
|
+
names = readdirSync(dir).filter((name) => name.endsWith(".json"));
|
|
1003
|
+
} catch {
|
|
1004
|
+
return [];
|
|
1005
|
+
}
|
|
1006
|
+
return names.map((name) => {
|
|
1007
|
+
const path = join(dir, name);
|
|
1008
|
+
let raw = null;
|
|
1009
|
+
try {
|
|
1010
|
+
raw = readFileSync(path, "utf8");
|
|
1011
|
+
} catch {}
|
|
1012
|
+
return parseAgentDeviceRecord(kind, path, raw);
|
|
1013
|
+
});
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
async function readAgentDeviceRecordsAsync(home, env = process.env) {
|
|
1017
|
+
const records = [];
|
|
1018
|
+
for (const { kind, dir } of agentDeviceDirs(home, env)) {
|
|
1019
|
+
let names;
|
|
1020
|
+
try {
|
|
1021
|
+
names = (await readdir(dir)).filter((name) => name.endsWith(".json"));
|
|
1022
|
+
} catch {
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
for (const name of names) {
|
|
1026
|
+
const path = join(dir, name);
|
|
1027
|
+
let raw = null;
|
|
1028
|
+
try {
|
|
1029
|
+
raw = await readFile(path, "utf8");
|
|
1030
|
+
} catch {}
|
|
1031
|
+
records.push(parseAgentDeviceRecord(kind, path, raw));
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return records;
|
|
1035
|
+
}
|
|
1036
|
+
function isClaimProtocolRunner(record, row) {
|
|
1037
|
+
if (!record.claimProtocol || record.runner?.pid !== row.pid || row.startedAt === null) return false;
|
|
1038
|
+
const recorded = Date.parse(record.runner.startTime);
|
|
1039
|
+
return Math.floor(recorded / 1e3) === Math.floor(Date.parse(row.startedAt) / 1e3);
|
|
1040
|
+
}
|
|
1041
|
+
function lsofArgs(port) {
|
|
1042
|
+
return [
|
|
1043
|
+
"-nP",
|
|
1044
|
+
`-iTCP:${port}`,
|
|
1045
|
+
"-sTCP:ESTABLISHED",
|
|
1046
|
+
"-Fpn"
|
|
1047
|
+
];
|
|
1048
|
+
}
|
|
1049
|
+
function lsofNoMatch(error) {
|
|
1050
|
+
const stdout = error.stdout;
|
|
1051
|
+
return error.status === 1 && (stdout === "" || stdout === void 0) ? "" : null;
|
|
1052
|
+
}
|
|
1053
|
+
function createDeviceProcessTables() {
|
|
1054
|
+
const exec = getExecutor();
|
|
1055
|
+
let host;
|
|
1056
|
+
const android = /* @__PURE__ */ new Map();
|
|
1057
|
+
const tcp = /* @__PURE__ */ new Map();
|
|
1058
|
+
return {
|
|
1059
|
+
tcpConnections(port) {
|
|
1060
|
+
if (!tcp.has(port)) {
|
|
1061
|
+
let output;
|
|
1062
|
+
try {
|
|
1063
|
+
output = exec.runFile("lsof", lsofArgs(port), { timeoutMs: 5e3 });
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
output = lsofNoMatch(error);
|
|
1066
|
+
}
|
|
1067
|
+
tcp.set(port, output);
|
|
1068
|
+
}
|
|
1069
|
+
return tcp.get(port) ?? null;
|
|
1070
|
+
},
|
|
1071
|
+
host() {
|
|
1072
|
+
if (host === void 0) {
|
|
1073
|
+
const output = exec.runFileQuiet("ps", [
|
|
1074
|
+
"-axww",
|
|
1075
|
+
"-o",
|
|
1076
|
+
HOST_PROCESS_COLUMNS
|
|
1077
|
+
], { timeoutMs: 1e4 });
|
|
1078
|
+
host = output === null ? null : parseProcessTable(output);
|
|
1079
|
+
}
|
|
1080
|
+
return host;
|
|
1081
|
+
},
|
|
1082
|
+
android(serial) {
|
|
1083
|
+
if (!android.has(serial)) android.set(serial, exec.runFileQuiet("adb", [
|
|
1084
|
+
"-s",
|
|
1085
|
+
serial,
|
|
1086
|
+
"shell",
|
|
1087
|
+
"ps",
|
|
1088
|
+
"-A",
|
|
1089
|
+
"-o",
|
|
1090
|
+
"PID,ARGS"
|
|
1091
|
+
], { timeoutMs: 5e3 }));
|
|
1092
|
+
return android.get(serial) ?? null;
|
|
1093
|
+
}
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
async function readHostProcessTableAsync(signal, operation) {
|
|
1097
|
+
const output = await runOperationFileQuiet(operation, getExecutor(), "ps", [
|
|
1098
|
+
"-axww",
|
|
1099
|
+
"-o",
|
|
1100
|
+
HOST_PROCESS_COLUMNS
|
|
1101
|
+
], {
|
|
1102
|
+
timeoutMs: 1e4,
|
|
1103
|
+
killSignal: "SIGKILL",
|
|
1104
|
+
signal,
|
|
1105
|
+
cwd: operation?.cwd
|
|
1106
|
+
});
|
|
1107
|
+
return output === null ? null : parseProcessTable(output);
|
|
1108
|
+
}
|
|
1109
|
+
/** Reads bounded host/instrumentation children asynchronously, sharing observations across device targets. */
|
|
1110
|
+
function createActivityReaderAsync(options = {}) {
|
|
1111
|
+
const exec = getExecutor();
|
|
1112
|
+
let host;
|
|
1113
|
+
let hostValue = null;
|
|
1114
|
+
const android = /* @__PURE__ */ new Map();
|
|
1115
|
+
const androidValues = /* @__PURE__ */ new Map();
|
|
1116
|
+
const read = createActivityReader({
|
|
1117
|
+
...options,
|
|
1118
|
+
tables: {
|
|
1119
|
+
...createDeviceProcessTables(),
|
|
1120
|
+
host: () => hostValue,
|
|
1121
|
+
android: (serial) => androidValues.get(serial) ?? null
|
|
1122
|
+
}
|
|
1123
|
+
});
|
|
1124
|
+
const probe = async (target) => {
|
|
1125
|
+
options.signal?.throwIfAborted();
|
|
1126
|
+
host ??= readHostProcessTableAsync(options.signal, options.operation);
|
|
1127
|
+
hostValue = await host;
|
|
1128
|
+
options.signal?.throwIfAborted();
|
|
1129
|
+
if (target.platform === "android") {
|
|
1130
|
+
let pending = android.get(target.id);
|
|
1131
|
+
if (!pending) {
|
|
1132
|
+
pending = runOperationFileQuiet(options.operation, exec, "adb", [
|
|
1133
|
+
"-s",
|
|
1134
|
+
target.id,
|
|
1135
|
+
"shell",
|
|
1136
|
+
"ps",
|
|
1137
|
+
"-A",
|
|
1138
|
+
"-o",
|
|
1139
|
+
"PID,ARGS"
|
|
1140
|
+
], {
|
|
1141
|
+
timeoutMs: 5e3,
|
|
1142
|
+
killSignal: "SIGKILL",
|
|
1143
|
+
signal: options.signal,
|
|
1144
|
+
cwd: process.platform === "win32" ? homedir() : options.operation?.cwd
|
|
1145
|
+
});
|
|
1146
|
+
android.set(target.id, pending);
|
|
1147
|
+
}
|
|
1148
|
+
androidValues.set(target.id, await pending);
|
|
1149
|
+
}
|
|
1150
|
+
options.signal?.throwIfAborted();
|
|
1151
|
+
return read(target);
|
|
1152
|
+
};
|
|
1153
|
+
let queued = Promise.resolve();
|
|
1154
|
+
return (target) => {
|
|
1155
|
+
if (!options.operation) return probe(target);
|
|
1156
|
+
const result = queued.then(() => probe(target));
|
|
1157
|
+
queued = result;
|
|
1158
|
+
return result;
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
function workspaceDeviceRecency(root) {
|
|
1162
|
+
const recency = [];
|
|
1163
|
+
const usedAt = Date.parse(String(readWorkspaceState$1(root)?.lastUsedAt ?? ""));
|
|
1164
|
+
if (Number.isFinite(usedAt)) recency.push({
|
|
1165
|
+
basis: "workspace-use",
|
|
1166
|
+
at: usedAt
|
|
1167
|
+
});
|
|
1168
|
+
for (const file of ["client.ndjson", "device.ndjson"]) try {
|
|
1169
|
+
recency.push({
|
|
1170
|
+
basis: "device-log",
|
|
1171
|
+
at: statSync(join(workspaceLogsDir$1(root), file)).mtimeMs
|
|
1172
|
+
});
|
|
1173
|
+
} catch {}
|
|
1174
|
+
return recency;
|
|
1175
|
+
}
|
|
1176
|
+
/** The newest workspace use or change to its in-app or native device logs. */
|
|
1177
|
+
function workspaceDeviceLastUseAt(root) {
|
|
1178
|
+
const times = workspaceDeviceRecency(root).map(({ at }) => at).filter(Number.isFinite);
|
|
1179
|
+
return times.length ? Math.max(...times) : NaN;
|
|
1180
|
+
}
|
|
1181
|
+
function createActivityReader({ now = Date.now(), env = process.env, home = env.HOME || env.USERPROFILE || homedir(), startOf = process_identity_exports.inspectProcessStart, leaseFiles = listLeaseFiles, tables = createDeviceProcessTables(), viewers = readViewedDevices } = {}) {
|
|
1182
|
+
let agentDevice;
|
|
1183
|
+
let stimLeases;
|
|
1184
|
+
let viewed;
|
|
1185
|
+
const logs = /* @__PURE__ */ new Map();
|
|
1186
|
+
const log = (path) => {
|
|
1187
|
+
if (!logs.has(path)) logs.set(path, tailLines(path));
|
|
1188
|
+
return logs.get(path) ?? null;
|
|
1189
|
+
};
|
|
1190
|
+
return (target) => {
|
|
1191
|
+
const evidence = {
|
|
1192
|
+
drivers: [],
|
|
1193
|
+
unknown: [],
|
|
1194
|
+
recency: []
|
|
1195
|
+
};
|
|
1196
|
+
agentDevice ??= readAgentDeviceRecords(home, env).map((record) => ({
|
|
1197
|
+
record,
|
|
1198
|
+
liveness: agentDeviceLiveness(record, startOf)
|
|
1199
|
+
}));
|
|
1200
|
+
for (const { record, liveness } of agentDevice) {
|
|
1201
|
+
if (record.deviceId !== null && record.deviceId !== target.id) continue;
|
|
1202
|
+
if (record.claimProtocol) continue;
|
|
1203
|
+
const basis = record.kind === "claim" ? "agent-device-claim" : "agent-device-lease";
|
|
1204
|
+
if (liveness === "live") {
|
|
1205
|
+
evidence.drivers.push({
|
|
1206
|
+
basis,
|
|
1207
|
+
tool: "agent-device",
|
|
1208
|
+
pid: record.owner?.pid ?? null,
|
|
1209
|
+
since: record.createdAtMs !== null ? new Date(record.createdAtMs).toISOString() : null
|
|
1210
|
+
});
|
|
1211
|
+
const actedAt = record.session ? agentSessionActedAt(home, record.session, record.stateDir, env) : null;
|
|
1212
|
+
if (actedAt !== null) evidence.recency.push({
|
|
1213
|
+
basis: "agent-action",
|
|
1214
|
+
at: actedAt
|
|
1215
|
+
});
|
|
1216
|
+
} else if (liveness === "unknown") evidence.unknown.push(basis);
|
|
1217
|
+
}
|
|
1218
|
+
try {
|
|
1219
|
+
stimLeases ??= leaseFiles();
|
|
1220
|
+
} catch {
|
|
1221
|
+
stimLeases = [];
|
|
1222
|
+
evidence.unknown.push("device-lock");
|
|
1223
|
+
}
|
|
1224
|
+
for (const entry of stimLeases) {
|
|
1225
|
+
if (entry.platform !== target.platform || entry.id !== target.id) continue;
|
|
1226
|
+
if (!entry.lease) evidence.unknown.push("device-lock");
|
|
1227
|
+
else if (!leaseIsExpired(entry.lease, now)) evidence.drivers.push({
|
|
1228
|
+
basis: "device-lock",
|
|
1229
|
+
tool: "stim device lock",
|
|
1230
|
+
pid: null,
|
|
1231
|
+
since: entry.lease.grantedAt
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
viewed ??= viewers();
|
|
1235
|
+
if (viewed.some((device) => device.platform === target.platform && device.id === target.id)) evidence.recency.push({
|
|
1236
|
+
basis: "viewer",
|
|
1237
|
+
at: now
|
|
1238
|
+
});
|
|
1239
|
+
const processes = tables.host();
|
|
1240
|
+
if (processes === null) evidence.unknown.push("driver-process");
|
|
1241
|
+
for (const row of processes ?? []) {
|
|
1242
|
+
if (agentDevice.some(({ record }) => isClaimProtocolRunner(record, row))) continue;
|
|
1243
|
+
const tool = hostDriverTool(row.command, target.platform, target.id);
|
|
1244
|
+
if (tool) evidence.drivers.push({
|
|
1245
|
+
basis: "driver-process",
|
|
1246
|
+
tool,
|
|
1247
|
+
pid: row.pid,
|
|
1248
|
+
since: row.startedAt
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
if (target.platform === "android") {
|
|
1252
|
+
const output = tables.android(target.id);
|
|
1253
|
+
if (output === null) evidence.unknown.push("instrumentation");
|
|
1254
|
+
for (const { pid, tool } of parseAndroidInstrumentation(output ?? "")) evidence.drivers.push({
|
|
1255
|
+
basis: "instrumentation",
|
|
1256
|
+
tool,
|
|
1257
|
+
pid,
|
|
1258
|
+
since: null
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
if (target.workspace) {
|
|
1262
|
+
const dir = workspaceLogsDir$1(target.workspace);
|
|
1263
|
+
const deviceAt = latestDeviceLogAt(log(join(dir, "device.ndjson")) ?? [], target);
|
|
1264
|
+
if (deviceAt !== null) evidence.recency.push({
|
|
1265
|
+
basis: "device-log",
|
|
1266
|
+
at: deviceAt
|
|
1267
|
+
});
|
|
1268
|
+
const bundleAt = latestBundleRequestAt(log(join(dir, "metro.ndjson")) ?? [], target.platform);
|
|
1269
|
+
if (bundleAt !== null) evidence.recency.push({
|
|
1270
|
+
basis: "metro-bundle",
|
|
1271
|
+
at: bundleAt
|
|
1272
|
+
});
|
|
1273
|
+
const usedAt = Date.parse(String(readWorkspaceState$1(target.workspace)?.lastUsedAt ?? ""));
|
|
1274
|
+
if (Number.isFinite(usedAt)) evidence.recency.push({
|
|
1275
|
+
basis: "workspace-use",
|
|
1276
|
+
at: usedAt
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
return classifyActivity(evidence, now);
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
function workspaceActivity(workspace, now = Date.now()) {
|
|
1283
|
+
const dir = workspaceLogsDir$1(workspace);
|
|
1284
|
+
const state = readWorkspaceState$1(workspace);
|
|
1285
|
+
const recency = [];
|
|
1286
|
+
const deviceAt = latestRecordAt(tailLines(join(dir, "device.ndjson")) ?? [], (record) => record.src === "device" && isDeviceRecord(record));
|
|
1287
|
+
if (deviceAt !== null) recency.push({
|
|
1288
|
+
basis: "device-log",
|
|
1289
|
+
at: deviceAt
|
|
1290
|
+
});
|
|
1291
|
+
const bundleAt = latestRecordAt(tailLines(join(dir, "metro.ndjson")) ?? [], (record) => record.event === "bundle_response_started");
|
|
1292
|
+
if (bundleAt !== null) recency.push({
|
|
1293
|
+
basis: "metro-bundle",
|
|
1294
|
+
at: bundleAt
|
|
1295
|
+
});
|
|
1296
|
+
for (const [basis, value] of [["workspace-use", state?.lastUsedAt], ["supervisor-start", state?.supervisor?.startedAt]]) {
|
|
1297
|
+
const at = Date.parse(String(value ?? ""));
|
|
1298
|
+
if (Number.isFinite(at)) recency.push({
|
|
1299
|
+
basis,
|
|
1300
|
+
at
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
return classifyActivity({
|
|
1304
|
+
drivers: [],
|
|
1305
|
+
unknown: [],
|
|
1306
|
+
recency
|
|
1307
|
+
}, now);
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* The pids of processes connected to loopback `port` as clients, from `lsof -Fpn` output: a connection whose remote
|
|
1311
|
+
* end is the port. Chrome's own accepted sockets have the port as their local end and are skipped.
|
|
1312
|
+
*/
|
|
1313
|
+
function parseTcpClients(output, port) {
|
|
1314
|
+
const pids = /* @__PURE__ */ new Set();
|
|
1315
|
+
let pid = null;
|
|
1316
|
+
for (const line of output.split("\n")) if (line.startsWith("p")) pid = Number(line.slice(1));
|
|
1317
|
+
else if (line.startsWith("n") && pid !== null && Number.isInteger(pid)) {
|
|
1318
|
+
const remote = line.slice(1).split("->")[1];
|
|
1319
|
+
if (remote && Number(remote.slice(remote.lastIndexOf(":") + 1)) === port) pids.add(pid);
|
|
1320
|
+
}
|
|
1321
|
+
return [...pids];
|
|
1322
|
+
}
|
|
1323
|
+
function webClientPids(connections, target) {
|
|
1324
|
+
return parseTcpClients(connections, target.port).filter((pid) => !target.ownPids.includes(pid));
|
|
1325
|
+
}
|
|
1326
|
+
function webDrivers(clients, processes) {
|
|
1327
|
+
return clients.flatMap((pid) => {
|
|
1328
|
+
const row = processes.find((candidate) => candidate.pid === pid);
|
|
1329
|
+
const tool = row ? cdpClientTool(row.command) : "unknown DevTools client";
|
|
1330
|
+
return tool ? [{
|
|
1331
|
+
tool,
|
|
1332
|
+
pid,
|
|
1333
|
+
since: row?.startedAt ?? null
|
|
1334
|
+
}] : [];
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* The DevTools clients of the owned browser that are not Stim's, read without blocking the event loop; null when
|
|
1339
|
+
* lsof or ps could not be read.
|
|
1340
|
+
*/
|
|
1341
|
+
async function probeWebDrivers(target) {
|
|
1342
|
+
const exec = getExecutor();
|
|
1343
|
+
let connections;
|
|
1344
|
+
try {
|
|
1345
|
+
connections = await exec.runFileAsync("lsof", lsofArgs(target.port), { timeoutMs: 5e3 });
|
|
1346
|
+
} catch (error) {
|
|
1347
|
+
connections = lsofNoMatch(error);
|
|
1348
|
+
}
|
|
1349
|
+
if (connections === null) return null;
|
|
1350
|
+
const clients = webClientPids(connections, target);
|
|
1351
|
+
if (!clients.length) return [];
|
|
1352
|
+
let table;
|
|
1353
|
+
try {
|
|
1354
|
+
table = await exec.runFileAsync("ps", [
|
|
1355
|
+
"-axww",
|
|
1356
|
+
"-o",
|
|
1357
|
+
HOST_PROCESS_COLUMNS
|
|
1358
|
+
], { timeoutMs: 5e3 });
|
|
1359
|
+
} catch {
|
|
1360
|
+
return null;
|
|
1361
|
+
}
|
|
1362
|
+
return webDrivers(clients, parseProcessTable(table));
|
|
1363
|
+
}
|
|
1364
|
+
/**
|
|
1365
|
+
* The owned browser's activity: a DevTools client other than Stim's is a driver (basis `cdp-client`). Recency comes
|
|
1366
|
+
* from the newest agent action the browser supervisor recorded (basis `agent-action`) and the newest other page
|
|
1367
|
+
* record (basis `page-log`).
|
|
1368
|
+
*/
|
|
1369
|
+
function readWebActivity(target, tables, now = Date.now()) {
|
|
1370
|
+
const evidence = {
|
|
1371
|
+
drivers: [],
|
|
1372
|
+
unknown: [],
|
|
1373
|
+
recency: []
|
|
1374
|
+
};
|
|
1375
|
+
const connections = tables.tcpConnections(target.port);
|
|
1376
|
+
const clients = connections === null ? [] : webClientPids(connections, target);
|
|
1377
|
+
const processes = clients.length ? (tables.clients ?? tables.host)() : [];
|
|
1378
|
+
if (connections === null || processes === null) evidence.unknown.push("cdp-client");
|
|
1379
|
+
else for (const driver of webDrivers(clients, processes)) evidence.drivers.push({
|
|
1380
|
+
...driver,
|
|
1381
|
+
basis: "cdp-client"
|
|
1382
|
+
});
|
|
1383
|
+
const lines = tailLines(join(workspaceLogsDir$1(target.workspace), "web.ndjson")) ?? [];
|
|
1384
|
+
const actionAt = latestRecordAt(lines, (record) => record.src === "agent");
|
|
1385
|
+
if (actionAt !== null) evidence.recency.push({
|
|
1386
|
+
basis: "agent-action",
|
|
1387
|
+
at: actionAt
|
|
1388
|
+
});
|
|
1389
|
+
const pageAt = latestRecordAt(lines, (record) => record.src !== "agent");
|
|
1390
|
+
if (pageAt !== null) evidence.recency.push({
|
|
1391
|
+
basis: "page-log",
|
|
1392
|
+
at: pageAt
|
|
1393
|
+
});
|
|
1394
|
+
return classifyActivity(evidence, now);
|
|
1395
|
+
}
|
|
1396
|
+
//#endregion
|
|
1397
|
+
export { selectPoolDevice as A, parseLeaseDuration as C, releaseWorkspaceLeases as D, releaseRunLease as E, waitForBuild as F, waitForSharedBuild as I, acquireBuildLock as M, listBuildLocks as N, releaseWorkspaceLeasesAsync as O, releaseBuildLock as P, parseLease as S, raiseLease as T, workspaceDeviceLastUseAt as _, createDeviceProcessTables as a, leaseIsExpired as b, parseAgentDeviceRecord as c, readAgentDeviceRecords as d, readAgentDeviceRecordsAsync as f, workspaceActivity as g, tailLines as h, createActivityReaderAsync as i, takeLease as j, removeExpiredLease as k, probeWebDrivers as l, readWebActivity as m, agentDeviceLiveness as n, envDir as o, readHostProcessTableAsync as p, createActivityReader as r, idleForMs as s, agentDeviceDaemon as t, processMatches as u, deviceLeasePath as v, parseWorkspaceLeases as w, listLeaseFiles as x, fileLeaseIo as y };
|