stim 1.12.0 → 1.13.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 +3 -1
- package/dist/{activity-CCZJs__P.mjs → activity-DfVKZTEB.mjs} +42 -25
- package/dist/{android-BQiKJYK0.mjs → android-BHfAu2mf.mjs} +1 -1
- package/dist/{android-B7X-8H7h.mjs → android-CD8pr8Q2.mjs} +62 -47
- package/dist/{android-cas-DzLPXiEn.mjs → android-cas-B9Kv9aIy.mjs} +9 -7
- package/dist/{android-75Sk2rIR.mjs → android-qx_31wJo.mjs} +395 -116
- package/dist/{app-install-CsZi0SAT.mjs → app-install-CDFY2nUL.mjs} +1 -1
- package/dist/{budget-DbzW5VTD.mjs → budget-E5o91NR1.mjs} +154 -37
- package/dist/{build-plan-CqntbR7b.mjs → build-plan-BTa93o_r.mjs} +21 -88
- package/dist/{build-progress-h60GPV27.mjs → build-progress-l7d-X-ZP.mjs} +75 -7
- package/dist/cdp-Dq_ypAlJ.mjs +115 -0
- package/dist/chrome-B5hWgQJS.mjs +61 -0
- package/dist/cli.mjs +16 -15
- package/dist/collector-run.mjs +14 -8
- package/dist/{command-output-BWCj5Vh-.mjs → command-output-BIL7rlX7.mjs} +1 -1
- package/dist/created-devices-Cd558h31.mjs +37 -0
- package/dist/{deps-CO-_7UOm.mjs → deps-2Wo81np9.mjs} +1 -1
- package/dist/{device-9r9xRYDa.mjs → device-Cf7vl-so.mjs} +6 -6
- package/dist/{device-lease-B9LAXIUH.mjs → device-lease-CL9dZf73.mjs} +52 -3
- package/dist/{device-pool-BoezJd-c.mjs → device-pool-MuoiyRcu.mjs} +3 -3
- package/dist/{device-remote-C0_o-tJH.mjs → device-remote-DT9ngZiD.mjs} +10 -10
- package/dist/{doctor-LfDlbR-N.mjs → doctor-D2iysNNH.mjs} +47 -18
- package/dist/{error-diagnostics-CCxjn31V.mjs → error-diagnostics-BoiOhck8.mjs} +6 -6
- package/dist/{gc-C4nQFcTX.mjs → gc-eA-PRBX8.mjs} +853 -126
- package/dist/{guide-BPBlh9ML.mjs → guide-BfZ0OmiP.mjs} +736 -110
- package/dist/{guide-status-i9ogHt4v.mjs → guide-status-BaMYu55M.mjs} +1 -1
- package/dist/{idle-S3tFZ53_.mjs → idle-DxLi5OkN.mjs} +42 -9
- package/dist/{in-use-D9XYEgFD.mjs → in-use-wYEaKQM9.mjs} +9 -5
- package/dist/{ios-CDzWL1mj.mjs → ios-CcfCeZ19.mjs} +98 -54
- package/dist/{ios-CZWOHTTc.mjs → ios-CmEzZiOj.mjs} +10 -5
- package/dist/{ios-device-D9cLxVr5.mjs → ios-device-BW-t_Lkv.mjs} +1 -1
- package/dist/{launch-verify-B2oR_kFN.mjs → launch-verify-CuJaBpEa.mjs} +33 -20
- package/dist/{logs-SzuYudOj.mjs → logs-DcSRbC2t.mjs} +11 -10
- package/dist/{metro-Dzo30hvJ.mjs → metro-BPeY1YMh.mjs} +4 -4
- package/dist/{named-ports-Dn1o61D9.mjs → named-ports-n7tnJsI8.mjs} +78 -6
- package/dist/native-runtime-CiiFHt0e.mjs +74 -0
- package/dist/{ownership-Dqnrjrio.mjs → ownership-Bz2kETAS.mjs} +1 -1
- package/dist/{ownership-B8d4kXBx.mjs → ownership-PUP4Dwai.mjs} +134 -209
- package/dist/page-BTu7I60K.mjs +29 -0
- package/dist/{ports-Dp_QTwTC.mjs → ports-C5UutqpJ.mjs} +4 -4
- package/dist/{project-B8rbutVC.mjs → project-DGeclhvO.mjs} +53 -17
- package/dist/{reload-nocmx8KB.mjs → reload-C-cV5yy_.mjs} +79 -20
- package/dist/{remote-cache-BRsRZru5.mjs → remote-cache--aehr4Pr.mjs} +3 -3
- package/dist/{server-bare-Gn3ssSdK.mjs → server-bare-Bq8BpSIo.mjs} +1 -1
- package/dist/server-expo-BeYwP_Xy.mjs +2 -0
- package/dist/{server-expo-CeT1nIIk.mjs → server-expo-DUCg52-K.mjs} +2 -2
- package/dist/{settings-bE-Gu3Ti.mjs → settings-ssVBToDR.mjs} +10 -10
- package/dist/{settings-BlyDn2Oz.mjs → settings-xB0Frkg6.mjs} +11 -2
- package/dist/settings.schema.json +53 -1
- package/dist/{simslim-COtEEP7x.mjs → simslim-Bbg9UWrt.mjs} +12 -6
- package/dist/{slot-launch-DIACGEfH.mjs → slot-launch-BIWEW-wI.mjs} +3 -3
- package/dist/{start-BboE1HIS.mjs → start-BZhItkiL.mjs} +11 -10
- package/dist/{start-BFstVBlx.mjs → start-Rw2GD3nV.mjs} +1 -1
- package/dist/{state-DcDgzfu3.mjs → state-DwAU2_-D.mjs} +1 -1
- package/dist/state-l1pYwOre.mjs +148 -0
- package/dist/{stats-DiCtmluY.mjs → stats-CaSwmsSZ.mjs} +2 -2
- package/dist/{status-DI9Tjo7T.mjs → status-TJlL0HWo.mjs} +624 -103
- package/dist/{status-BUASYyy_.mjs → status-sszP4XKU.mjs} +59 -2
- package/dist/{stim-desktop-D0xhpZt8.mjs → stim-desktop-CXt8-Z3J.mjs} +13 -2
- package/dist/{stop-Bn6v-GvS.mjs → stop-BZ2qMiBP.mjs} +37 -12
- package/dist/{stop-BI7QY76M.mjs → stop-DskDKNTU.mjs} +2 -2
- package/dist/supervisor-run.mjs +10 -10
- package/dist/web-run.d.mts +22 -0
- package/dist/web-run.mjs +481 -0
- package/dist/web-sWXd5iek.mjs +394 -0
- package/dist/{worktree-B5sTRskL.mjs → worktree-3cBa-bQt.mjs} +1 -1
- package/dist/{worktree-ChDL5fO1.mjs → worktree-D4Do_XiQ.mjs} +226 -19
- package/dist/{xcode-BzplpQzc.mjs → xcode-LM6A8cNZ.mjs} +5 -5
- package/package.json +5 -5
- package/dist/server-expo-eWi9Coof.mjs +0 -2
- package/dist/workspace-process-lock-BvvJTfo_.mjs +0 -57
|
@@ -1,67 +1,81 @@
|
|
|
1
|
-
import { I as volumeRootFor, U as projectDeviceSlots, _ as loadConfig$1, d as getConfigDir } from "./config-0jCcMM0E.mjs";
|
|
1
|
+
import { I as volumeRootFor, U as projectDeviceSlots, _ as loadConfig$1, d as getConfigDir, z as deviceSlotKey } from "./config-0jCcMM0E.mjs";
|
|
2
2
|
import { t as getExecutor } from "./exec-DK8KaVHK.mjs";
|
|
3
3
|
import { a as parseNdjsonLine } from "./ndjson-CDY9IkVC.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { M as workspaceLogsDir, c as resolveProjectMetro, h as readWorkspaceState$1, i as listeningPidsByPort, j as workspaceLogErrorIndex, o as processCwd, r as listeningPids, s as processCwds, u as isMetroRunning } from "./metro-BPeY1YMh.mjs";
|
|
5
5
|
import { c as gitCommonDirOnDisk, p as linkedWorktreesOnDisk, s as gitCommonDir, y as repoRoot } from "./worktree-DeBQSVtR.mjs";
|
|
6
|
-
import {
|
|
7
|
-
import { f as readWorkspaceLaunches, u as readRemoteSession } from "./state-
|
|
8
|
-
import { i as formatDuration } from "./command-output-
|
|
9
|
-
import { i as listLeaseFiles } from "./device-lease-
|
|
10
|
-
import { n as createActivityReader } from "./activity-
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
6
|
+
import { l as projectShortcut, o as findServerWorkspace, r as detectIsExpo } from "./project-DGeclhvO.mjs";
|
|
7
|
+
import { f as readWorkspaceLaunches, u as readRemoteSession } from "./state-DwAU2_-D.mjs";
|
|
8
|
+
import { i as formatDuration } from "./command-output-BIL7rlX7.mjs";
|
|
9
|
+
import { i as listLeaseFiles } from "./device-lease-CL9dZf73.mjs";
|
|
10
|
+
import { n as createActivityReader, r as createDeviceProcessTables } from "./activity-DfVKZTEB.mjs";
|
|
11
|
+
import { P as ownedAvdDeviceProfile, R as ownedAvdSerialResolver, ct as parkedMaxSetting, lt as readParked, rt as POOL_SETTING_REMEDY, u as androidToolPath } from "./android-qx_31wJo.mjs";
|
|
12
|
+
import { t as resolveSupervisorTarget } from "./ownership-PUP4Dwai.mjs";
|
|
13
|
+
import { a as readWebRecord, l as webFacts } from "./state-l1pYwOre.mjs";
|
|
14
|
+
import { _ as parseSimctlList, u as listAllIosSimsAsync } from "./ios-CmEzZiOj.mjs";
|
|
15
|
+
import { a as buildStatusLine, i as buildReport, r as activeBuildState, s as parseActiveBuild, t as ACTIVE_BUILD_KEY } from "./build-progress-l7d-X-ZP.mjs";
|
|
16
|
+
import { T as readEasSessionLedger$1, w as easMachineStateRoot$1 } from "./device-remote-DT9ngZiD.mjs";
|
|
17
|
+
import { _ as withWebFacts, a as deviceLeaseStates, d as poolLine, f as remoteDeviceLine, g as unprovisionedWorktrees, h as tightVolumes, i as deviceLeaseLines, l as gitSummaryText, m as statusActivity, n as activityLabel, o as diskLine, p as remoteDeviceState, r as capacity, s as environmentState, u as parseDfFree } from "./status-sszP4XKU.mjs";
|
|
17
18
|
import { a as statsProjectKey, r as readStats } from "./stats-D8rUTPHd.mjs";
|
|
18
19
|
import { n as mergeState } from "./merge-state-ytFQCeh4.mjs";
|
|
19
|
-
import { i as readSupervisorState } from "./stop-
|
|
20
|
-
import { mkdirSync, readFileSync, readdirSync, renameSync, rmSync, watch, writeFileSync } from "node:fs";
|
|
20
|
+
import { i as readSupervisorState } from "./stop-BZ2qMiBP.mjs";
|
|
21
|
+
import { mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
|
|
21
22
|
import { closeSync as closeSync$1, existsSync as existsSync$1, fstatSync as fstatSync$1, openSync as openSync$1, readFileSync as readFileSync$1, readSync as readSync$1, renameSync as renameSync$1, rmSync as rmSync$1, writeFileSync as writeFileSync$1 } from "fs";
|
|
22
23
|
import { basename, dirname, join } from "path";
|
|
23
24
|
import { rotatedLogPath } from "@stim-cli/core";
|
|
24
|
-
import { ERROR_SOURCES, gitMergeCacheDir, logFiles, queryLogs, readIdleStop, readLastBuilds, recordMatches } from "@stim-cli/core/state";
|
|
25
|
-
import { join as join$1, posix } from "node:path";
|
|
25
|
+
import { ERROR_SOURCES, MACHINE_OWNER_KINDS, gitMergeCacheDir, logFiles, queryLogs, readBuildHistory, readIdleStop, readLastBuilds, recordMatches, withStateReadCache } from "@stim-cli/core/state";
|
|
26
|
+
import { join as join$1, posix, resolve as resolve$1 } from "node:path";
|
|
26
27
|
import { createHash, randomUUID } from "node:crypto";
|
|
27
28
|
import { homedir, totalmem } from "os";
|
|
28
29
|
import chalk from "chalk";
|
|
29
30
|
//#region src/status-watch.ts
|
|
30
31
|
const WATCH_FALLBACK_MS = 3e4;
|
|
31
|
-
const
|
|
32
|
+
const WATCH_LIGHT_INTERVAL_MS = 15e3;
|
|
33
|
+
const WATCH_SIMCTL_INTERVAL_MS = 5e3;
|
|
32
34
|
const ADB_RESTART_MIN_MS = 1e3;
|
|
33
35
|
const ADB_RESTART_MAX_MS = 6e4;
|
|
34
36
|
const SIMCTL_TIMEOUT_MS = 1e4;
|
|
35
37
|
/**
|
|
36
|
-
* Coalesces change signals into refreshes: a burst of triggers runs `run` once
|
|
37
|
-
*
|
|
38
|
-
*
|
|
38
|
+
* Coalesces change signals into refreshes: a burst of full triggers runs `run('full')` once after `debounceMs` of
|
|
39
|
+
* quiet, at most one run is in flight, and a trigger that arrives during a run causes exactly one more run after it.
|
|
40
|
+
* A `light` trigger runs `run('light')` no sooner than `debounceMs` from the trigger and `lightIntervalMs` from the
|
|
41
|
+
* start of the previous run, unless a queued full run covers it; later light triggers do not postpone it.
|
|
39
42
|
*/
|
|
40
|
-
function createRefreshScheduler({ debounceMs, run }) {
|
|
43
|
+
function createRefreshScheduler({ debounceMs, lightIntervalMs = 0, run }) {
|
|
41
44
|
let timer = null;
|
|
45
|
+
let queued = "full";
|
|
42
46
|
let running = false;
|
|
43
|
-
let pending =
|
|
47
|
+
let pending = null;
|
|
44
48
|
let stopped = false;
|
|
49
|
+
let lastRunAt = -Infinity;
|
|
45
50
|
const fire = async () => {
|
|
46
51
|
timer = null;
|
|
47
52
|
running = true;
|
|
53
|
+
lastRunAt = Date.now();
|
|
48
54
|
try {
|
|
49
|
-
await run();
|
|
55
|
+
await run(queued);
|
|
50
56
|
} finally {
|
|
51
57
|
running = false;
|
|
52
58
|
if (pending) {
|
|
53
|
-
|
|
54
|
-
|
|
59
|
+
const next = pending;
|
|
60
|
+
pending = null;
|
|
61
|
+
trigger(next);
|
|
55
62
|
}
|
|
56
63
|
}
|
|
57
64
|
};
|
|
58
|
-
function trigger(delayMs = debounceMs) {
|
|
65
|
+
function trigger(kind = "full", delayMs = debounceMs) {
|
|
59
66
|
if (stopped) return;
|
|
60
67
|
if (running) {
|
|
61
|
-
pending =
|
|
68
|
+
pending = pending === "full" ? "full" : kind;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (kind === "light") {
|
|
72
|
+
if (timer) return;
|
|
73
|
+
queued = "light";
|
|
74
|
+
timer = setTimeout(() => void fire(), Math.max(delayMs, lastRunAt + lightIntervalMs - Date.now()));
|
|
62
75
|
return;
|
|
63
76
|
}
|
|
64
77
|
if (timer) clearTimeout(timer);
|
|
78
|
+
queued = "full";
|
|
65
79
|
timer = setTimeout(() => void fire(), delayMs);
|
|
66
80
|
}
|
|
67
81
|
return {
|
|
@@ -74,17 +88,19 @@ function createRefreshScheduler({ debounceMs, run }) {
|
|
|
74
88
|
};
|
|
75
89
|
}
|
|
76
90
|
/**
|
|
77
|
-
*
|
|
78
|
-
* the
|
|
91
|
+
* Which refresh a change to `name` in a watched `$STIM_HOME` directory needs: `light` for a log append, which can
|
|
92
|
+
* change only the log-derived fields, `full` for anything else that can change the payload, null for none. A null
|
|
93
|
+
* name means the platform did not report one.
|
|
79
94
|
*/
|
|
80
|
-
function
|
|
81
|
-
if (name
|
|
82
|
-
if (
|
|
95
|
+
function statusChange(dir, name) {
|
|
96
|
+
if (name?.includes(".lock")) return null;
|
|
97
|
+
if (dir === "logs") return "light";
|
|
98
|
+
if (name === null) return "full";
|
|
83
99
|
switch (dir) {
|
|
84
|
-
case "home": return name.startsWith("config.json") || name === "workspaces" || name === "device-locks";
|
|
85
|
-
case "workspace": return name.startsWith("state.json") || name === "logs";
|
|
86
|
-
case "eas": return name.startsWith("sessions.json");
|
|
87
|
-
default: return
|
|
100
|
+
case "home": return name.startsWith("config.json") || name === "workspaces" || name === "device-locks" ? "full" : null;
|
|
101
|
+
case "workspace": return name.startsWith("state.json") || name === "logs" ? "full" : null;
|
|
102
|
+
case "eas": return name.startsWith("sessions.json") ? "full" : null;
|
|
103
|
+
default: return "full";
|
|
88
104
|
}
|
|
89
105
|
}
|
|
90
106
|
/** A stable summary of every simulator's state, or null when the listing is unreadable. */
|
|
@@ -132,9 +148,13 @@ function watchStatusSources({ home, onChange, platform = process.platform, simct
|
|
|
132
148
|
if (watchers.has(dir)) continue;
|
|
133
149
|
try {
|
|
134
150
|
const watcher = watch(dir, (_event, name) => {
|
|
135
|
-
|
|
136
|
-
if (
|
|
137
|
-
|
|
151
|
+
const change = statusChange(kind, name === null ? null : String(name));
|
|
152
|
+
if (!change) return;
|
|
153
|
+
if (kind === "home" || kind === "workspaces" || kind === "workspace") {
|
|
154
|
+
listingStaleBefore = Date.now();
|
|
155
|
+
reconcile();
|
|
156
|
+
}
|
|
157
|
+
onChange(change);
|
|
138
158
|
});
|
|
139
159
|
watcher.on("error", () => {
|
|
140
160
|
watcher.close();
|
|
@@ -144,13 +164,16 @@ function watchStatusSources({ home, onChange, platform = process.platform, simct
|
|
|
144
164
|
} catch {}
|
|
145
165
|
}
|
|
146
166
|
};
|
|
147
|
-
const adb = trackAdbDevices(onChange);
|
|
167
|
+
const adb = trackAdbDevices(() => onChange("full"));
|
|
148
168
|
let simctl = null;
|
|
169
|
+
let listing = null;
|
|
170
|
+
let listingStaleBefore = 0;
|
|
149
171
|
let simTimer = null;
|
|
150
172
|
if (platform === "darwin") {
|
|
151
173
|
let last = null;
|
|
152
174
|
const check = () => {
|
|
153
175
|
if (simctl || stopped) return;
|
|
176
|
+
const startedAt = Date.now();
|
|
154
177
|
let out = "";
|
|
155
178
|
const child = getExecutor().spawn("xcrun", [
|
|
156
179
|
"simctl",
|
|
@@ -175,7 +198,11 @@ function watchStatusSources({ home, onChange, platform = process.platform, simct
|
|
|
175
198
|
simctl = null;
|
|
176
199
|
const signature = simulatorSignature(out);
|
|
177
200
|
if (signature === null || stopped) return;
|
|
178
|
-
|
|
201
|
+
listing = {
|
|
202
|
+
at: startedAt,
|
|
203
|
+
json: out
|
|
204
|
+
};
|
|
205
|
+
if (last !== null && signature !== last) onChange("full");
|
|
179
206
|
last = signature;
|
|
180
207
|
});
|
|
181
208
|
};
|
|
@@ -185,6 +212,7 @@ function watchStatusSources({ home, onChange, platform = process.platform, simct
|
|
|
185
212
|
reconcile();
|
|
186
213
|
return {
|
|
187
214
|
reconcile,
|
|
215
|
+
simulatorListing: () => listing && listing.at > listingStaleBefore && Date.now() - listing.at <= 2 * simctlIntervalMs ? listing.json : null,
|
|
188
216
|
stop() {
|
|
189
217
|
stopped = true;
|
|
190
218
|
for (const watcher of watchers.values()) watcher.close();
|
|
@@ -238,7 +266,7 @@ function trackAdbDevices(onChange) {
|
|
|
238
266
|
}
|
|
239
267
|
//#endregion
|
|
240
268
|
//#region src/diagnostics/error-index.ts
|
|
241
|
-
const INDEX_VERSION =
|
|
269
|
+
const INDEX_VERSION = 3;
|
|
242
270
|
const HEAD_BYTES = 1024;
|
|
243
271
|
const TAIL_BYTES = 256;
|
|
244
272
|
const CHUNK_BYTES = 4194304;
|
|
@@ -251,7 +279,7 @@ const CANDIDATE_PATTERNS = [
|
|
|
251
279
|
const ERROR_CRITERIA = {
|
|
252
280
|
errorsOnly: true,
|
|
253
281
|
sources: ERROR_SOURCES,
|
|
254
|
-
|
|
282
|
+
includeAppDeviceErrors: true
|
|
255
283
|
};
|
|
256
284
|
/**
|
|
257
285
|
* The number of records `queryLogs({ dir, errorsOnly: true })` returns, without parsing every
|
|
@@ -291,8 +319,9 @@ function summarize(path, known) {
|
|
|
291
319
|
return null;
|
|
292
320
|
}
|
|
293
321
|
try {
|
|
294
|
-
const
|
|
295
|
-
const key = `${dev}:${ino}`;
|
|
322
|
+
const stat = fstatSync$1(fd, { bigint: true });
|
|
323
|
+
const key = `${stat.dev}:${stat.ino}`;
|
|
324
|
+
const size = Number(stat.size);
|
|
296
325
|
const cached = known[key];
|
|
297
326
|
const base = cached && stillExtends(fd, cached, size) ? cached : null;
|
|
298
327
|
const start = base ?? {
|
|
@@ -370,12 +399,17 @@ function collect(record, markers, errors) {
|
|
|
370
399
|
const ts = typeof record.ts === "number" && Number.isFinite(record.ts) ? record.ts : null;
|
|
371
400
|
if (record.marker === true && ts !== null) {
|
|
372
401
|
const metro = record.src === "metro";
|
|
402
|
+
const web = !metro && record.platform === "web";
|
|
373
403
|
const slot = record.slot ?? "default";
|
|
374
|
-
const key = metro ? "metro" : JSON.stringify(["launch", slot]);
|
|
404
|
+
const key = metro ? "metro" : web ? "web" : JSON.stringify(["launch", slot]);
|
|
375
405
|
if ((markers[key]?.ts ?? -Infinity) < ts) markers[key] = metro ? {
|
|
376
406
|
src: "metro",
|
|
377
407
|
marker: true,
|
|
378
408
|
ts
|
|
409
|
+
} : web ? {
|
|
410
|
+
platform: "web",
|
|
411
|
+
marker: true,
|
|
412
|
+
ts
|
|
379
413
|
} : {
|
|
380
414
|
marker: true,
|
|
381
415
|
ts,
|
|
@@ -386,6 +420,7 @@ function collect(record, markers, errors) {
|
|
|
386
420
|
src: record.src,
|
|
387
421
|
level: record.level,
|
|
388
422
|
event: record.event,
|
|
423
|
+
platform: record.platform,
|
|
389
424
|
ts: ts ?? void 0,
|
|
390
425
|
slot: record.slot
|
|
391
426
|
});
|
|
@@ -413,6 +448,7 @@ function writeIndex(file, index) {
|
|
|
413
448
|
//#region src/workspace/git-summary.ts
|
|
414
449
|
const GIT_TIMEOUT_MS = 3e3;
|
|
415
450
|
const MERGE_BUDGET_MS = 250;
|
|
451
|
+
const MERGE_TIMEOUT_BACKOFF_MS = 3e5;
|
|
416
452
|
const ORIGIN_PREFIX = "refs/remotes/origin/";
|
|
417
453
|
/** Counts the entries of `git status --porcelain=v2 --branch` output. `head` is null on an unborn branch. */
|
|
418
454
|
function parseGitStatus(text) {
|
|
@@ -508,11 +544,33 @@ function mergedInto(path, head, target, budget) {
|
|
|
508
544
|
const targetKey = `${target.ref} ${target.sha}`;
|
|
509
545
|
const cached = readMergeCache(path);
|
|
510
546
|
const known = cached?.head === head ? cached : null;
|
|
511
|
-
if (known?.target === targetKey) return
|
|
512
|
-
|
|
547
|
+
if (known?.target === targetKey) return {
|
|
548
|
+
into: known.mergedInto,
|
|
549
|
+
deferred: false
|
|
550
|
+
};
|
|
551
|
+
const standIn = known?.target.startsWith(`${target.ref} `) ? known.mergedInto : null;
|
|
552
|
+
const timeoutKey = `${head} ${targetKey}`;
|
|
553
|
+
const timedOut = mergeTimeouts.get(path);
|
|
554
|
+
if (timedOut?.key === timeoutKey && Date.now() - timedOut.at < MERGE_TIMEOUT_BACKOFF_MS) return {
|
|
555
|
+
into: standIn,
|
|
556
|
+
deferred: false
|
|
557
|
+
};
|
|
558
|
+
if (budget.deadline !== void 0 && Date.now() > budget.deadline) return {
|
|
559
|
+
into: standIn,
|
|
560
|
+
deferred: true
|
|
561
|
+
};
|
|
513
562
|
budget.deadline ??= Date.now() + MERGE_BUDGET_MS;
|
|
514
563
|
const state = mergeState(path, target, { timeoutMs: GIT_TIMEOUT_MS });
|
|
515
|
-
if (!state.merged && state.timedOut)
|
|
564
|
+
if (!state.merged && state.timedOut) {
|
|
565
|
+
mergeTimeouts.set(path, {
|
|
566
|
+
key: timeoutKey,
|
|
567
|
+
at: Date.now()
|
|
568
|
+
});
|
|
569
|
+
return {
|
|
570
|
+
into: standIn,
|
|
571
|
+
deferred: false
|
|
572
|
+
};
|
|
573
|
+
}
|
|
516
574
|
const verdict = state.merged ? state.into : null;
|
|
517
575
|
writeMergeCache({
|
|
518
576
|
path,
|
|
@@ -520,29 +578,97 @@ function mergedInto(path, head, target, budget) {
|
|
|
520
578
|
target: targetKey,
|
|
521
579
|
mergedInto: verdict
|
|
522
580
|
});
|
|
523
|
-
return
|
|
581
|
+
return {
|
|
582
|
+
into: verdict,
|
|
583
|
+
deferred: false
|
|
584
|
+
};
|
|
524
585
|
}
|
|
586
|
+
const mergeTimeouts = /* @__PURE__ */ new Map();
|
|
525
587
|
const recent = /* @__PURE__ */ new Map();
|
|
588
|
+
function gitDirsOf(worktree) {
|
|
589
|
+
try {
|
|
590
|
+
const dotGit = join$1(worktree, ".git");
|
|
591
|
+
const pointer = statSync(dotGit).isDirectory() ? null : /^gitdir: (.+)$/m.exec(readFileSync(dotGit, "utf-8"))?.[1]?.trim();
|
|
592
|
+
const gitDir = pointer ? resolve$1(worktree, pointer) : pointer === null ? dotGit : null;
|
|
593
|
+
if (!gitDir) return null;
|
|
594
|
+
try {
|
|
595
|
+
return {
|
|
596
|
+
gitDir,
|
|
597
|
+
commonDir: resolve$1(gitDir, readFileSync(join$1(gitDir, "commondir"), "utf-8").trim())
|
|
598
|
+
};
|
|
599
|
+
} catch {
|
|
600
|
+
return {
|
|
601
|
+
gitDir,
|
|
602
|
+
commonDir: gitDir
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
} catch {
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
function fileStamp(path) {
|
|
610
|
+
try {
|
|
611
|
+
const stat = statSync(path, { bigint: true });
|
|
612
|
+
return `${stat.ino}:${stat.mtimeNs}:${stat.size}`;
|
|
613
|
+
} catch {
|
|
614
|
+
return "-";
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Stamps the git files a summary depends on: the worktree's index, HEAD and reflog, and the common dir's packed refs,
|
|
619
|
+
* last fetch, origin/HEAD, default-branch ref, branch ref and upstream ref. Edits, creations and deletions of files
|
|
620
|
+
* that are not staged touch none of them. `missing` when the worktree has no git dir, such as a deleted worktree git
|
|
621
|
+
* still lists; it is then stamped by its `.git` entry alone.
|
|
622
|
+
*/
|
|
623
|
+
function gitFilesStamp(worktree, refs) {
|
|
624
|
+
const dirs = gitDirsOf(worktree.path);
|
|
625
|
+
if (!dirs) return {
|
|
626
|
+
stamp: fileStamp(join$1(worktree.path, ".git")),
|
|
627
|
+
missing: true
|
|
628
|
+
};
|
|
629
|
+
const { gitDir, commonDir } = dirs;
|
|
630
|
+
return {
|
|
631
|
+
stamp: [
|
|
632
|
+
join$1(gitDir, "index"),
|
|
633
|
+
join$1(gitDir, "HEAD"),
|
|
634
|
+
join$1(gitDir, "logs", "HEAD"),
|
|
635
|
+
join$1(commonDir, "packed-refs"),
|
|
636
|
+
join$1(commonDir, "FETCH_HEAD"),
|
|
637
|
+
join$1(commonDir, "refs", "remotes", "origin", "HEAD"),
|
|
638
|
+
...refs.target ? [join$1(commonDir, refs.target)] : [],
|
|
639
|
+
...worktree.branch ? [join$1(commonDir, "refs", "heads", worktree.branch)] : [],
|
|
640
|
+
...refs.upstream ? [join$1(commonDir, "refs", "remotes", refs.upstream), join$1(commonDir, "refs", "heads", refs.upstream)] : []
|
|
641
|
+
].map(fileStamp).join(" "),
|
|
642
|
+
missing: false
|
|
643
|
+
};
|
|
644
|
+
}
|
|
526
645
|
/**
|
|
527
646
|
* Reads every worktree's git summary in parallel, each git call bounded by a timeout. A worktree `skip` rejects, or
|
|
528
647
|
* one git cannot answer in time, maps to null. After every read, merge verdicts missing from the cache are judged one
|
|
529
648
|
* at a time; no new judgement starts 250 ms after the first, and a verdict for the same HEAD judged at an older
|
|
530
|
-
* default-branch commit stands in, because a merged branch stays merged.
|
|
531
|
-
* recently in this process is
|
|
649
|
+
* default-branch commit stands in, because a merged branch stays merged. A judgement that timed out is not retried
|
|
650
|
+
* for the same HEAD and target for five minutes. With `maxAgeMs`, a summary made that recently in this process is
|
|
651
|
+
* reused while the git files it depends on are unchanged, so a change that is not staged can take up to `maxAgeMs`
|
|
652
|
+
* to show; so is the null of a worktree whose directory is gone.
|
|
532
653
|
*/
|
|
533
654
|
async function readWorktreeGit(worktrees, { skip, maxAgeMs = 0 }) {
|
|
534
655
|
const now = Date.now();
|
|
535
656
|
const targets = /* @__PURE__ */ new Map();
|
|
536
657
|
const reads = await Promise.all(worktrees.map(async (worktree) => {
|
|
537
|
-
const memo = recent.get(worktree.path);
|
|
538
|
-
if (memo && now - memo.at < maxAgeMs) return {
|
|
539
|
-
path: worktree.path,
|
|
540
|
-
memo: memo.git
|
|
541
|
-
};
|
|
542
658
|
if (skip(worktree)) return {
|
|
543
659
|
path: worktree.path,
|
|
544
660
|
memo: null
|
|
545
661
|
};
|
|
662
|
+
const memo = maxAgeMs > 0 ? recent.get(worktree.path) : void 0;
|
|
663
|
+
const refs = memo?.refs ?? {
|
|
664
|
+
upstream: null,
|
|
665
|
+
target: null
|
|
666
|
+
};
|
|
667
|
+
const files = maxAgeMs > 0 ? gitFilesStamp(worktree, refs) : null;
|
|
668
|
+
if (memo && memo.files !== null && memo.files === files?.stamp && now - memo.at < maxAgeMs) return {
|
|
669
|
+
path: worktree.path,
|
|
670
|
+
memo: memo.git
|
|
671
|
+
};
|
|
546
672
|
const repository = worktree.repository;
|
|
547
673
|
if (repository && !targets.has(repository)) targets.set(repository, defaultBranchOf(repository));
|
|
548
674
|
const [out, target] = await Promise.all([git(worktree.path, [
|
|
@@ -554,23 +680,43 @@ async function readWorktreeGit(worktrees, { skip, maxAgeMs = 0 }) {
|
|
|
554
680
|
return {
|
|
555
681
|
path: worktree.path,
|
|
556
682
|
status: out === null ? null : parseGitStatus(out),
|
|
557
|
-
target
|
|
683
|
+
target,
|
|
684
|
+
files,
|
|
685
|
+
refs
|
|
558
686
|
};
|
|
559
687
|
}));
|
|
560
688
|
const budget = {};
|
|
561
689
|
const summaries = /* @__PURE__ */ new Map();
|
|
562
690
|
for (const read of reads) {
|
|
563
|
-
if ("memo" in read
|
|
564
|
-
summaries.set(read.path,
|
|
691
|
+
if ("memo" in read) {
|
|
692
|
+
summaries.set(read.path, read.memo ?? null);
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
if (!read.status) {
|
|
696
|
+
if (read.files?.missing) recent.set(read.path, {
|
|
697
|
+
at: now,
|
|
698
|
+
files: read.files.stamp,
|
|
699
|
+
refs: read.refs,
|
|
700
|
+
git: null
|
|
701
|
+
});
|
|
702
|
+
summaries.set(read.path, null);
|
|
565
703
|
continue;
|
|
566
704
|
}
|
|
567
705
|
const { head, ...counts } = read.status;
|
|
706
|
+
const merge = head && read.target ? mergedInto(read.path, head, read.target, budget) : null;
|
|
568
707
|
const summary = {
|
|
569
708
|
...counts,
|
|
570
|
-
mergedInto:
|
|
709
|
+
mergedInto: merge?.into ?? null
|
|
710
|
+
};
|
|
711
|
+
const refs = {
|
|
712
|
+
upstream: counts.upstream,
|
|
713
|
+
target: read.target?.ref ?? null
|
|
571
714
|
};
|
|
715
|
+
const settled = !merge?.deferred && refs.upstream === read.refs.upstream && refs.target === read.refs.target;
|
|
572
716
|
recent.set(read.path, {
|
|
573
717
|
at: now,
|
|
718
|
+
files: settled ? read.files?.stamp ?? null : null,
|
|
719
|
+
refs,
|
|
574
720
|
git: summary
|
|
575
721
|
});
|
|
576
722
|
summaries.set(read.path, summary);
|
|
@@ -578,25 +724,268 @@ async function readWorktreeGit(worktrees, { skip, maxAgeMs = 0 }) {
|
|
|
578
724
|
return summaries;
|
|
579
725
|
}
|
|
580
726
|
//#endregion
|
|
727
|
+
//#region src/devices/app-process.ts
|
|
728
|
+
const bundleIdsByApp = /* @__PURE__ */ new Map();
|
|
729
|
+
function simAppPaths(commands, udid) {
|
|
730
|
+
const marker = `/Devices/${udid}/data/Containers/Bundle/Application/`;
|
|
731
|
+
const paths = /* @__PURE__ */ new Set();
|
|
732
|
+
for (const command of commands) {
|
|
733
|
+
const at = command.indexOf(marker);
|
|
734
|
+
if (!command.startsWith("/") || at < 0 || /[\s=]\//.test(command.slice(0, at))) continue;
|
|
735
|
+
const rest = command.slice(at + marker.length);
|
|
736
|
+
const app = /^[^/]+\/[^/]+\.app\//.exec(rest)?.[0];
|
|
737
|
+
if (!app || /^[^/\s]*\//.test(rest.slice(app.length))) continue;
|
|
738
|
+
paths.add(command.slice(0, at + marker.length) + app.slice(0, -1));
|
|
739
|
+
}
|
|
740
|
+
return [...paths];
|
|
741
|
+
}
|
|
742
|
+
function androidAppRunning(psOutput, packageName) {
|
|
743
|
+
return psOutput.split("\n").some((line) => {
|
|
744
|
+
return /^\s*\d+\s+(\S+)\s*$/.exec(line)?.[1] === packageName;
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
function plistBundleId(appPath) {
|
|
748
|
+
const cached = bundleIdsByApp.get(appPath);
|
|
749
|
+
if (cached) return cached;
|
|
750
|
+
const id = getExecutor().runFileQuiet("plutil", [
|
|
751
|
+
"-extract",
|
|
752
|
+
"CFBundleIdentifier",
|
|
753
|
+
"raw",
|
|
754
|
+
"-o",
|
|
755
|
+
"-",
|
|
756
|
+
join$1(appPath, "Info.plist")
|
|
757
|
+
], { timeoutMs: 2e3 })?.trim() || null;
|
|
758
|
+
if (id) bundleIdsByApp.set(appPath, id);
|
|
759
|
+
return id;
|
|
760
|
+
}
|
|
761
|
+
function createAppProcessReader(tables, bundleIdOf = plistBundleId) {
|
|
762
|
+
return ({ platform, id, appId }) => {
|
|
763
|
+
const result = (state) => ({
|
|
764
|
+
id: appId,
|
|
765
|
+
state
|
|
766
|
+
});
|
|
767
|
+
if (platform === "android") {
|
|
768
|
+
const output = tables.android(id);
|
|
769
|
+
if (output === null) return result("unknown");
|
|
770
|
+
return result(androidAppRunning(output, appId) ? "running" : "stopped");
|
|
771
|
+
}
|
|
772
|
+
const host = tables.host();
|
|
773
|
+
if (host === null) return result("unknown");
|
|
774
|
+
const ids = simAppPaths(host.map((row) => row.command), id).map(bundleIdOf);
|
|
775
|
+
if (ids.includes(appId)) return result("running");
|
|
776
|
+
return result(ids.includes(null) ? "unknown" : "stopped");
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
//#endregion
|
|
780
|
+
//#region src/machine-usage.ts
|
|
781
|
+
const SHARED = [
|
|
782
|
+
{
|
|
783
|
+
name: "CoreSimulator services",
|
|
784
|
+
pattern: /\/CoreSimulator\.framework\/|\/Simulator\.app\/Contents\/MacOS\//
|
|
785
|
+
},
|
|
786
|
+
{
|
|
787
|
+
name: "adb server",
|
|
788
|
+
pattern: /(^|\/)adb\s.*\bfork-server\b/
|
|
789
|
+
},
|
|
790
|
+
{
|
|
791
|
+
name: "Android emulator services",
|
|
792
|
+
pattern: /\/emulator\/netsimd(\s|$)/
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
name: "Gradle daemon",
|
|
796
|
+
pattern: /\borg\.gradle\.launcher\.daemon\.bootstrap\.GradleDaemon\b/
|
|
797
|
+
},
|
|
798
|
+
{
|
|
799
|
+
name: "Kotlin daemon",
|
|
800
|
+
pattern: /\bKotlinCompileDaemon\b/
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
name: "Watchman",
|
|
804
|
+
pattern: /(^|\/)watchman(\s|$)/
|
|
805
|
+
}
|
|
806
|
+
];
|
|
807
|
+
const SERVER = /(^|[\s/])stim-server(\.mjs)?(\s|$)/;
|
|
808
|
+
const LAUNCHD_SIM = /^launchd_sim\s.*\/Devices\/([^/\s]+)\//;
|
|
809
|
+
const MAX_DEPTH = 64;
|
|
810
|
+
const EMULATOR = /(?:^|\/)(?:qemu-system-[^\s/]+|emulator)(?:\s.*)?\s(?:-avd\s+|@)(\S+)/;
|
|
811
|
+
function emulatorAvd(command) {
|
|
812
|
+
return EMULATOR.exec(command)?.[1] ?? null;
|
|
813
|
+
}
|
|
814
|
+
function workspaceOwner(kind, env, name, id) {
|
|
815
|
+
return {
|
|
816
|
+
kind,
|
|
817
|
+
name,
|
|
818
|
+
workspace: env.path,
|
|
819
|
+
id,
|
|
820
|
+
owned: true
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
function rank(owner) {
|
|
824
|
+
return MACHINE_OWNER_KINDS.indexOf(owner.kind);
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Attributes every process in `processes` to at most one owner: the owner of its nearest ancestor (itself included)
|
|
828
|
+
* that is a root. Roots are each booted simulator's `launchd_sim`, each emulator launcher and qemu process, and each
|
|
829
|
+
* workspace's verified supervisor and Metro, running build and Chrome. Only a process under none of those falls back
|
|
830
|
+
* to stim-server or a machine-wide service in `SHARED`, so a `simctl` a workspace's log collector runs stays with the
|
|
831
|
+
* workspace. A process with no root ancestor is left out.
|
|
832
|
+
*/
|
|
833
|
+
function attributeMachineUsage({ processes, environments, roots, simNames }) {
|
|
834
|
+
const owners = /* @__PURE__ */ new Map();
|
|
835
|
+
const rootOf = /* @__PURE__ */ new Map();
|
|
836
|
+
const fallbackOf = /* @__PURE__ */ new Map();
|
|
837
|
+
const claim = (pid, key, owner, into = rootOf) => {
|
|
838
|
+
if (!owners.has(key)) owners.set(key, owner);
|
|
839
|
+
into.set(pid, key);
|
|
840
|
+
};
|
|
841
|
+
const sims = /* @__PURE__ */ new Map();
|
|
842
|
+
const emulators = /* @__PURE__ */ new Map();
|
|
843
|
+
for (const env of environments) for (const device of [{
|
|
844
|
+
slot: "default",
|
|
845
|
+
ios: env.ios,
|
|
846
|
+
android: env.android
|
|
847
|
+
}, ...env.slots ?? []]) {
|
|
848
|
+
const slot = device.slot === "default" ? {} : { slot: device.slot };
|
|
849
|
+
if (device.ios?.state === "Booted") sims.set(device.ios.udid.toUpperCase(), {
|
|
850
|
+
kind: "simulator",
|
|
851
|
+
name: device.ios.name ?? device.ios.udid,
|
|
852
|
+
workspace: env.path,
|
|
853
|
+
...slot,
|
|
854
|
+
id: device.ios.udid,
|
|
855
|
+
owned: device.ios.owned
|
|
856
|
+
});
|
|
857
|
+
if (device.android?.name && !device.android.physical && device.android.serial) emulators.set(device.android.name, {
|
|
858
|
+
kind: "emulator",
|
|
859
|
+
name: device.android.name,
|
|
860
|
+
workspace: env.path,
|
|
861
|
+
...slot,
|
|
862
|
+
id: device.android.name,
|
|
863
|
+
owned: device.android.owned
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
for (const p of processes) {
|
|
867
|
+
const shared = SHARED.find(({ pattern }) => pattern.test(p.command));
|
|
868
|
+
if (shared) {
|
|
869
|
+
const owner = {
|
|
870
|
+
kind: "shared",
|
|
871
|
+
name: shared.name,
|
|
872
|
+
workspace: null,
|
|
873
|
+
id: null,
|
|
874
|
+
owned: false
|
|
875
|
+
};
|
|
876
|
+
claim(p.pid, `shared:${shared.name}`, owner, fallbackOf);
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
if (SERVER.test(p.command)) {
|
|
880
|
+
claim(p.pid, "server", {
|
|
881
|
+
kind: "server",
|
|
882
|
+
name: "stim-server",
|
|
883
|
+
workspace: null,
|
|
884
|
+
id: null,
|
|
885
|
+
owned: false
|
|
886
|
+
}, fallbackOf);
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
const udid = LAUNCHD_SIM.exec(p.command)?.[1]?.toUpperCase();
|
|
890
|
+
if (udid) {
|
|
891
|
+
const owner = sims.get(udid) ?? {
|
|
892
|
+
kind: "simulator",
|
|
893
|
+
name: simNames[udid] ?? udid,
|
|
894
|
+
workspace: null,
|
|
895
|
+
id: udid,
|
|
896
|
+
owned: false
|
|
897
|
+
};
|
|
898
|
+
claim(p.pid, `simulator:${udid}`, owner);
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
const avd = emulatorAvd(p.command);
|
|
902
|
+
if (avd) {
|
|
903
|
+
const owner = emulators.get(avd) ?? {
|
|
904
|
+
kind: "emulator",
|
|
905
|
+
name: avd,
|
|
906
|
+
workspace: null,
|
|
907
|
+
id: avd,
|
|
908
|
+
owned: false
|
|
909
|
+
};
|
|
910
|
+
claim(p.pid, `emulator:${avd}`, owner);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
const present = new Set(processes.map((p) => p.pid));
|
|
914
|
+
const byPath = new Map(environments.map((env) => [env.path, env]));
|
|
915
|
+
for (const root of roots) {
|
|
916
|
+
const env = byPath.get(root.path);
|
|
917
|
+
if (!env) continue;
|
|
918
|
+
const metroPids = [root.supervisorPid, env.metro?.running ? env.metro.pid : null];
|
|
919
|
+
for (const pid of metroPids) if (pid && present.has(pid)) {
|
|
920
|
+
const port = env.metro?.port ?? null;
|
|
921
|
+
claim(pid, `metro:${env.path}`, workspaceOwner("metro", env, port ? `Metro :${port}` : "Metro", port ? String(port) : null));
|
|
922
|
+
}
|
|
923
|
+
if (root.build && present.has(root.build.pid)) {
|
|
924
|
+
const label = root.build.platform === "ios" ? "iOS build" : "Android build";
|
|
925
|
+
claim(root.build.pid, `build:${env.path}`, workspaceOwner("build", env, label, root.build.platform));
|
|
926
|
+
}
|
|
927
|
+
for (const pid of root.browserPids) if (present.has(pid)) claim(pid, `browser:${env.path}`, workspaceOwner("browser", env, "Chrome", null));
|
|
928
|
+
}
|
|
929
|
+
const parentOf = new Map(processes.map((p) => [p.pid, p.ppid]));
|
|
930
|
+
const totals = /* @__PURE__ */ new Map();
|
|
931
|
+
for (const p of processes) {
|
|
932
|
+
let pid = p.pid;
|
|
933
|
+
let key;
|
|
934
|
+
let fallback;
|
|
935
|
+
for (let depth = 0; pid !== void 0 && pid > 1 && depth < MAX_DEPTH; depth++) {
|
|
936
|
+
key = rootOf.get(pid);
|
|
937
|
+
if (key) break;
|
|
938
|
+
fallback ??= fallbackOf.get(pid);
|
|
939
|
+
pid = parentOf.get(pid);
|
|
940
|
+
}
|
|
941
|
+
key ??= fallback;
|
|
942
|
+
if (!key) continue;
|
|
943
|
+
const total = totals.get(key) ?? {
|
|
944
|
+
rssKb: 0,
|
|
945
|
+
cpu: 0,
|
|
946
|
+
processes: 0
|
|
947
|
+
};
|
|
948
|
+
total.rssKb += p.rssKb;
|
|
949
|
+
total.cpu += p.cpuPercent;
|
|
950
|
+
total.processes += 1;
|
|
951
|
+
totals.set(key, total);
|
|
952
|
+
}
|
|
953
|
+
const list = [];
|
|
954
|
+
for (const [key, owner] of owners) {
|
|
955
|
+
const total = totals.get(key);
|
|
956
|
+
if (total) list.push({
|
|
957
|
+
...owner,
|
|
958
|
+
cpuPercent: Math.round(total.cpu),
|
|
959
|
+
residentMb: Math.round(total.rssKb / 1024),
|
|
960
|
+
processes: total.processes
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
list.sort((x, y) => rank(x) - rank(y) || (x.workspace ?? "~").localeCompare(y.workspace ?? "~") || x.name.localeCompare(y.name));
|
|
964
|
+
return { owners: list };
|
|
965
|
+
}
|
|
966
|
+
//#endregion
|
|
581
967
|
//#region src/commands/status.ts
|
|
582
|
-
const WATCH_GIT_MAX_AGE_MS =
|
|
968
|
+
const WATCH_GIT_MAX_AGE_MS = 6e4;
|
|
583
969
|
function formatGb(mb) {
|
|
584
970
|
return `${(mb / 1024).toFixed(1)} GB`;
|
|
585
971
|
}
|
|
586
972
|
function statusCommand(program) {
|
|
587
973
|
program.command("status").description("Show every environment on this machine: devices, ports, what is actually running, and anything stuck.").option("--json", "print the state as JSON").option("--watch", "keep running and print the state again each time it changes").action(async (opts) => {
|
|
588
974
|
if (opts.watch) return watchStatus(Boolean(opts.json));
|
|
589
|
-
|
|
975
|
+
const json = Boolean(opts.json);
|
|
976
|
+
for (const line of renderStatus(await readStatus(0), json)) console.log(line);
|
|
590
977
|
});
|
|
591
978
|
}
|
|
592
|
-
|
|
593
|
-
|
|
979
|
+
function readStatus(gitMaxAgeMs, simctlListing = null) {
|
|
980
|
+
return withStateReadCache(() => readStatusFacts(gitMaxAgeMs, simctlListing));
|
|
981
|
+
}
|
|
982
|
+
async function readStatusFacts(gitMaxAgeMs, simctlListing) {
|
|
594
983
|
const cfg = loadConfig$1();
|
|
595
984
|
const projects = Object.entries(cfg?.projects || {});
|
|
596
|
-
const cwdRoot =
|
|
985
|
+
const cwdRoot = findServerWorkspace(process.cwd())?.root ?? null;
|
|
597
986
|
const worktrees = linkedWorktrees([process.cwd(), ...projects.map(([path]) => path)]);
|
|
598
987
|
const orphanWorktrees = unprovisionedWorktrees(worktrees, projects.map(([p]) => p));
|
|
599
|
-
const simsRead = listAllIosSimsAsync();
|
|
988
|
+
const simsRead = simctlListing === null ? listAllIosSimsAsync() : Promise.resolve().then(() => parseSimctlList(simctlListing));
|
|
600
989
|
simsRead.catch(() => {});
|
|
601
990
|
const ports = await portLookup(projects.flatMap(([, proj]) => proj.metroPort ? [proj.metroPort] : []));
|
|
602
991
|
const processes = Promise.all(projects.map(async ([path, proj]) => {
|
|
@@ -630,6 +1019,7 @@ async function statusLines(json, gitMaxAgeMs) {
|
|
|
630
1019
|
const history = readStats().record?.history;
|
|
631
1020
|
const states = [];
|
|
632
1021
|
const labelOnlyRoots = [];
|
|
1022
|
+
const launchesByState = [];
|
|
633
1023
|
const easLedger = readEasSessionLedger$1();
|
|
634
1024
|
const leaseNow = Date.now();
|
|
635
1025
|
const leaseFiles = listLeaseFiles();
|
|
@@ -637,11 +1027,25 @@ async function statusLines(json, gitMaxAgeMs) {
|
|
|
637
1027
|
root: cwdRoot,
|
|
638
1028
|
now: leaseNow
|
|
639
1029
|
});
|
|
1030
|
+
const roots = [];
|
|
640
1031
|
for (const [i, [path, proj]] of projects.entries()) {
|
|
641
1032
|
const { metro, supervisor } = running[i];
|
|
642
1033
|
const saved = readWorkspaceState$1(path);
|
|
643
1034
|
const builds = workspaceBuilds(path, saved, history);
|
|
644
|
-
|
|
1035
|
+
const web = webFacts(readWebRecord(path));
|
|
1036
|
+
const activeBuild = parseActiveBuild(saved?.[ACTIVE_BUILD_KEY]);
|
|
1037
|
+
roots.push({
|
|
1038
|
+
path,
|
|
1039
|
+
supervisorPid: supervisor?.status === "ours" ? supervisor.pid : null,
|
|
1040
|
+
build: activeBuild && builds.build?.state === "running" ? {
|
|
1041
|
+
platform: activeBuild.platform,
|
|
1042
|
+
pid: activeBuild.claim.pid
|
|
1043
|
+
} : null,
|
|
1044
|
+
browserPids: browserPids(web)
|
|
1045
|
+
});
|
|
1046
|
+
const launches = readWorkspaceLaunches(path);
|
|
1047
|
+
launchesByState.push(launches);
|
|
1048
|
+
states.push(withWebFacts(environmentState({
|
|
645
1049
|
...proj,
|
|
646
1050
|
__path: path
|
|
647
1051
|
}, {
|
|
@@ -654,38 +1058,86 @@ async function statusLines(json, gitMaxAgeMs) {
|
|
|
654
1058
|
logs: logs[i],
|
|
655
1059
|
remote: remoteDeviceState(readRemoteSession(path), easLedger, path),
|
|
656
1060
|
idleStop: readIdleStop(saved),
|
|
657
|
-
launches
|
|
1061
|
+
launches,
|
|
658
1062
|
leasedIds: new Set(deviceLeaseStates(leaseFiles, {
|
|
659
1063
|
root: path,
|
|
660
1064
|
now: leaseNow
|
|
661
1065
|
}).flatMap((lease) => lease.mine && !lease.expired && lease.id ? [lease.id] : [])),
|
|
662
1066
|
now: leaseNow
|
|
663
|
-
}));
|
|
1067
|
+
}), web));
|
|
664
1068
|
const state = states[states.length - 1];
|
|
665
1069
|
if (state) Object.assign(state, builds);
|
|
666
|
-
labelOnlyRoots.push(Boolean(proj.worktreeRoot && !proj.bundleId && !state?.metro && !state?.ios && !state?.android));
|
|
667
|
-
}
|
|
668
|
-
const readActivity = createActivityReader();
|
|
669
|
-
for (const state of states) for (const device of [{
|
|
670
|
-
slot: "default",
|
|
671
|
-
ios: state.ios,
|
|
672
|
-
android: state.android
|
|
673
|
-
}, ...state.slots ?? []]) {
|
|
674
|
-
const base = {
|
|
675
|
-
slot: device.slot,
|
|
676
|
-
workspace: state.path
|
|
677
|
-
};
|
|
678
|
-
if (device.ios?.state === "Booted") device.ios.activity = readActivity({
|
|
679
|
-
...base,
|
|
680
|
-
platform: "ios",
|
|
681
|
-
id: device.ios.udid
|
|
682
|
-
});
|
|
683
|
-
if (device.android && !device.android.physical && device.android.serial) device.android.activity = readActivity({
|
|
684
|
-
...base,
|
|
685
|
-
platform: "android",
|
|
686
|
-
id: device.android.serial
|
|
687
|
-
});
|
|
1070
|
+
labelOnlyRoots.push(Boolean(proj.worktreeRoot && !proj.bundleId && !state?.metro && !state?.ios && !state?.android && !state?.web));
|
|
688
1071
|
}
|
|
1072
|
+
const tables = createDeviceProcessTables();
|
|
1073
|
+
readDeviceProcesses(states, tables, {
|
|
1074
|
+
projects,
|
|
1075
|
+
launchesByState
|
|
1076
|
+
});
|
|
1077
|
+
const sims = Object.values(simsByUdid);
|
|
1078
|
+
const simNames = Object.fromEntries(sims.map((sim) => [sim.udid.toUpperCase(), sim.name]));
|
|
1079
|
+
const machine = readMachineUsage({
|
|
1080
|
+
states,
|
|
1081
|
+
roots,
|
|
1082
|
+
simNames,
|
|
1083
|
+
tables,
|
|
1084
|
+
busy: sims.some((sim) => sim.state === "Booted")
|
|
1085
|
+
});
|
|
1086
|
+
return {
|
|
1087
|
+
projects,
|
|
1088
|
+
states,
|
|
1089
|
+
labelOnlyRoots,
|
|
1090
|
+
leases,
|
|
1091
|
+
leaseNow,
|
|
1092
|
+
orphanWorktrees,
|
|
1093
|
+
simsAvailable,
|
|
1094
|
+
simctlError,
|
|
1095
|
+
cwdRoot,
|
|
1096
|
+
tables,
|
|
1097
|
+
machine,
|
|
1098
|
+
roots,
|
|
1099
|
+
simNames
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
function browserPids(web) {
|
|
1103
|
+
if (!web) return [];
|
|
1104
|
+
const chrome = web.record.chromeProcess?.pid;
|
|
1105
|
+
if (web.status === "running") return [web.record.pid, ...chrome ? [chrome] : []];
|
|
1106
|
+
return web.status === "orphaned" && chrome ? [chrome] : [];
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Attributes the host process table to its owners. Reads the table only when a simulator is booted, a workspace is
|
|
1110
|
+
* live or a build runs, so an idle machine runs no `ps`.
|
|
1111
|
+
*/
|
|
1112
|
+
function readMachineUsage({ states, roots, simNames, tables, busy }) {
|
|
1113
|
+
const processes = busy || states.some((state) => state.live || state.build?.state === "running") ? tables.host() : null;
|
|
1114
|
+
return processes ? attributeMachineUsage({
|
|
1115
|
+
processes,
|
|
1116
|
+
environments: states,
|
|
1117
|
+
roots,
|
|
1118
|
+
simNames
|
|
1119
|
+
}) : null;
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Rereads only the error counts, device activity and machine usage. With `machine`, and when the snapshot measured
|
|
1123
|
+
* machine usage, it reads the host process table again; it reuses every other subprocess fact of the full read.
|
|
1124
|
+
*/
|
|
1125
|
+
function refreshLightFacts(snapshot, machine) {
|
|
1126
|
+
if (machine && snapshot.machine) snapshot.tables = {
|
|
1127
|
+
host: createDeviceProcessTables().host,
|
|
1128
|
+
android: snapshot.tables.android
|
|
1129
|
+
};
|
|
1130
|
+
withStateReadCache(() => {
|
|
1131
|
+
for (const state of snapshot.states) state.logs = logFacts(state.path);
|
|
1132
|
+
readDeviceProcesses(snapshot.states, snapshot.tables, null);
|
|
1133
|
+
});
|
|
1134
|
+
if (machine && snapshot.machine) snapshot.machine = readMachineUsage({
|
|
1135
|
+
...snapshot,
|
|
1136
|
+
busy: true
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
function renderStatus({ projects, states, labelOnlyRoots, leases, leaseNow, orphanWorktrees, simsAvailable, simctlError, cwdRoot, machine }, json) {
|
|
1140
|
+
const out = [];
|
|
689
1141
|
const totalMemoryMb = Math.round(totalmem() / 1048576);
|
|
690
1142
|
const cap = capacity(states, totalMemoryMb);
|
|
691
1143
|
const pools = ["ios", "android"].map((platform) => {
|
|
@@ -708,7 +1160,8 @@ async function statusLines(json, gitMaxAgeMs) {
|
|
|
708
1160
|
capacity: cap,
|
|
709
1161
|
deviceLeases: leases,
|
|
710
1162
|
unprovisionedWorktrees: orphanWorktrees,
|
|
711
|
-
simctlAvailable: simsAvailable
|
|
1163
|
+
simctlAvailable: simsAvailable,
|
|
1164
|
+
machine
|
|
712
1165
|
}));
|
|
713
1166
|
return out;
|
|
714
1167
|
}
|
|
@@ -729,7 +1182,7 @@ async function statusLines(json, gitMaxAgeMs) {
|
|
|
729
1182
|
const marker = path === cwdRoot ? chalk.bold.cyan(`* ${shortcut}`) : shortcut;
|
|
730
1183
|
const idle = state.live ? "" : chalk.dim(" [idle]");
|
|
731
1184
|
out.push(`\n${marker}${idle} ${chalk.dim(`(${path})`)}`);
|
|
732
|
-
out.push(labelOnlyRoots[i] ? chalk.dim(" worktree root (holds the label; the app registers its own entry)") : chalk.dim(` app: ${proj.bundleId ?? "?"} (${proj.isExpo ? "expo" : "bare"})`));
|
|
1185
|
+
out.push(labelOnlyRoots[i] ? chalk.dim(" worktree root (holds the label; the app registers its own entry)") : chalk.dim(` app: ${proj.bundleId ?? "?"} (${proj.isExpo ?? detectIsExpo(path) ? "expo" : "bare"})`));
|
|
733
1186
|
if (state.metro) {
|
|
734
1187
|
const label = state.metro.running ? chalk.green(`running (pid ${state.metro.pid})`) : state.metro.idleStop ? chalk.dim(`stopped (idle) after ${state.metro.idleStop.idleMinutes}m; \`stim start\`, \`ios\` or \`android\` restarts it`) : chalk.dim("not running");
|
|
735
1188
|
out.push(` metro: port ${state.metro.port} ${label}`);
|
|
@@ -759,14 +1212,18 @@ async function statusLines(json, gitMaxAgeMs) {
|
|
|
759
1212
|
if (deviceState.ios) {
|
|
760
1213
|
const booted = deviceState.ios.state === "Booted" ? chalk.green("booted") : chalk.dim(deviceState.ios.state.toLowerCase());
|
|
761
1214
|
const owned = deviceState.ios.owned ? chalk.dim(" (owned)") : "";
|
|
762
|
-
out.push(` ios${slotLabel}: ${chalk.cyan(deviceState.ios.name ?? deviceState.ios.udid)} ${booted}${owned}${activitySuffix(deviceState.ios.activity)}`);
|
|
1215
|
+
out.push(` ios${slotLabel}: ${chalk.cyan(deviceState.ios.name ?? deviceState.ios.udid)} ${booted}${owned}${activitySuffix(deviceState.ios.activity)}${appSuffix(deviceState.ios.app)}`);
|
|
763
1216
|
}
|
|
764
1217
|
if (deviceState.android) {
|
|
765
1218
|
const kind = deviceState.android.physical ? chalk.dim("(physical)") : chalk.dim("(emulator)");
|
|
766
1219
|
const observed = deviceState.android.state ? ` ${deviceState.android.state}${deviceState.android.serial ? ` (${deviceState.android.serial})` : ""}` : "";
|
|
767
|
-
out.push(` android${slotLabel}: ${chalk.cyan(deviceState.android.name)} ${kind}${observed}${deviceState.android.owned ? chalk.dim(" (owned)") : ""}${activitySuffix(deviceState.android.activity)}`);
|
|
1220
|
+
out.push(` android${slotLabel}: ${chalk.cyan(deviceState.android.name)} ${kind}${observed}${deviceState.android.owned ? chalk.dim(" (owned)") : ""}${activitySuffix(deviceState.android.activity)}${appSuffix(deviceState.android.app)}`);
|
|
768
1221
|
}
|
|
769
1222
|
}
|
|
1223
|
+
if (state.web) {
|
|
1224
|
+
const browser = state.web.running ? chalk.green(`running (pid ${state.web.pid}, DevTools ${state.web.cdpEndpoint})`) : chalk.dim("not running");
|
|
1225
|
+
out.push(` web: ${chalk.cyan(state.web.url)} ${browser}${state.web.headless ? "" : chalk.dim(" (headed)")}`);
|
|
1226
|
+
}
|
|
770
1227
|
for (const remote of state.remoteDevices ?? []) out.push(` ${remoteDeviceLine(remote)}`);
|
|
771
1228
|
for (const w of state.warnings) out.push(chalk.yellow(` ! ${w}`));
|
|
772
1229
|
}
|
|
@@ -798,13 +1255,17 @@ async function statusLines(json, gitMaxAgeMs) {
|
|
|
798
1255
|
}
|
|
799
1256
|
async function watchStatus(json) {
|
|
800
1257
|
let last = null;
|
|
1258
|
+
let snapshot = null;
|
|
801
1259
|
let sources = null;
|
|
802
1260
|
const scheduler = createRefreshScheduler({
|
|
803
1261
|
debounceMs: 250,
|
|
804
|
-
|
|
1262
|
+
lightIntervalMs: WATCH_LIGHT_INTERVAL_MS,
|
|
1263
|
+
run: async (kind) => {
|
|
805
1264
|
let text;
|
|
806
1265
|
try {
|
|
807
|
-
|
|
1266
|
+
if (kind === "light" && snapshot) refreshLightFacts(snapshot, json);
|
|
1267
|
+
else snapshot = await readStatus(WATCH_GIT_MAX_AGE_MS, sources?.simulatorListing());
|
|
1268
|
+
text = renderStatus(snapshot, json).join("\n");
|
|
808
1269
|
} catch (error) {
|
|
809
1270
|
console.error(chalk.red(String(error?.message || error)));
|
|
810
1271
|
return;
|
|
@@ -816,9 +1277,11 @@ async function watchStatus(json) {
|
|
|
816
1277
|
process.stdout.write(`${!json && process.stdout.isTTY ? "\x1B[2J\x1B[H" : ""}${text}\n`);
|
|
817
1278
|
}
|
|
818
1279
|
});
|
|
819
|
-
const fallback = setInterval(() => scheduler.trigger(), WATCH_FALLBACK_MS);
|
|
1280
|
+
const fallback = setInterval(() => scheduler.trigger("full"), WATCH_FALLBACK_MS);
|
|
1281
|
+
const machine = setInterval(() => json && snapshot?.machine && scheduler.trigger("light"), WATCH_LIGHT_INTERVAL_MS);
|
|
820
1282
|
const finish = () => {
|
|
821
1283
|
clearInterval(fallback);
|
|
1284
|
+
clearInterval(machine);
|
|
822
1285
|
scheduler.stop();
|
|
823
1286
|
sources?.stop();
|
|
824
1287
|
process.exit(0);
|
|
@@ -828,9 +1291,9 @@ async function watchStatus(json) {
|
|
|
828
1291
|
process.stdout.on("error", finish);
|
|
829
1292
|
sources = watchStatusSources({
|
|
830
1293
|
home: getConfigDir(),
|
|
831
|
-
onChange: () => scheduler.trigger()
|
|
1294
|
+
onChange: (kind) => scheduler.trigger(kind)
|
|
832
1295
|
});
|
|
833
|
-
scheduler.trigger(0);
|
|
1296
|
+
scheduler.trigger("full", 0);
|
|
834
1297
|
await new Promise(() => {});
|
|
835
1298
|
}
|
|
836
1299
|
async function readGitInto(worktrees, orphans, maxAgeMs) {
|
|
@@ -857,6 +1320,62 @@ function activitySuffix(activity) {
|
|
|
857
1320
|
const label = activityLabel(activity, Date.now());
|
|
858
1321
|
return label ? ` -- ${activity?.state === "driven" ? chalk.magenta(label) : chalk.dim(label)}` : "";
|
|
859
1322
|
}
|
|
1323
|
+
function readDeviceProcesses(states, tables, apps) {
|
|
1324
|
+
const readActivity = createActivityReader({ tables });
|
|
1325
|
+
const readAppProcess = createAppProcessReader(tables);
|
|
1326
|
+
for (const [i, state] of states.entries()) {
|
|
1327
|
+
const appIdOn = (platform, slot, deviceId) => {
|
|
1328
|
+
if (!apps) return void 0;
|
|
1329
|
+
const project = apps.projects[i][1];
|
|
1330
|
+
const launch = apps.launchesByState[i][deviceSlotKey(platform, slot)];
|
|
1331
|
+
return launch?.deviceId === deviceId ? launch.appId : platform === "ios" ? project.bundleId : project.androidPackage;
|
|
1332
|
+
};
|
|
1333
|
+
for (const device of [{
|
|
1334
|
+
slot: "default",
|
|
1335
|
+
ios: state.ios,
|
|
1336
|
+
android: state.android
|
|
1337
|
+
}, ...state.slots ?? []]) {
|
|
1338
|
+
const base = {
|
|
1339
|
+
slot: device.slot,
|
|
1340
|
+
workspace: state.path
|
|
1341
|
+
};
|
|
1342
|
+
if (device.ios?.state === "Booted") {
|
|
1343
|
+
const id = device.ios.udid;
|
|
1344
|
+
device.ios.activity = statusActivity(readActivity({
|
|
1345
|
+
...base,
|
|
1346
|
+
platform: "ios",
|
|
1347
|
+
id
|
|
1348
|
+
}));
|
|
1349
|
+
const appId = device.ios.owned ? appIdOn("ios", device.slot, id) : void 0;
|
|
1350
|
+
if (appId) device.ios.app = readAppProcess({
|
|
1351
|
+
platform: "ios",
|
|
1352
|
+
id,
|
|
1353
|
+
appId
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
if (device.android && !device.android.physical && device.android.serial) {
|
|
1357
|
+
const id = device.android.serial;
|
|
1358
|
+
device.android.activity = statusActivity(readActivity({
|
|
1359
|
+
...base,
|
|
1360
|
+
platform: "android",
|
|
1361
|
+
id
|
|
1362
|
+
}));
|
|
1363
|
+
const appId = device.android.owned ? appIdOn("android", device.slot, id) : void 0;
|
|
1364
|
+
if (appId) device.android.app = readAppProcess({
|
|
1365
|
+
platform: "android",
|
|
1366
|
+
id,
|
|
1367
|
+
appId
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
function appSuffix(app) {
|
|
1374
|
+
if (!app) return "";
|
|
1375
|
+
if (app.state === "running") return chalk.dim(` -- ${app.id} running`);
|
|
1376
|
+
if (app.state === "stopped") return ` -- ${chalk.yellow(`${app.id} not running`)}`;
|
|
1377
|
+
return chalk.dim(` -- ${app.id} process unknown`);
|
|
1378
|
+
}
|
|
860
1379
|
function linkedWorktrees(paths) {
|
|
861
1380
|
return [...new Set(paths.flatMap((path) => gitCommonDirOnDisk(path) ?? []))].flatMap((common) => {
|
|
862
1381
|
const repository = basename(common) === ".git" ? dirname(common) : common;
|
|
@@ -865,9 +1384,11 @@ function linkedWorktrees(paths) {
|
|
|
865
1384
|
}
|
|
866
1385
|
function workspaceBuilds(path, saved, history) {
|
|
867
1386
|
const lastBuilds = readLastBuilds(saved);
|
|
1387
|
+
const builds = readBuildHistory(saved);
|
|
868
1388
|
return {
|
|
869
1389
|
build: workspaceBuild(path, saved, history),
|
|
870
|
-
...lastBuilds.ios || lastBuilds.android ? { lastBuilds } : {}
|
|
1390
|
+
...lastBuilds.ios || lastBuilds.android ? { lastBuilds } : {},
|
|
1391
|
+
...builds.ios || builds.android ? { builds } : {}
|
|
871
1392
|
};
|
|
872
1393
|
}
|
|
873
1394
|
function workspaceBuild(path, saved, history) {
|