@stim-cli/core 1.16.0 → 1.17.1
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/dist/{index-DPo8Bexj.d.mts → index-BTYoTeax.d.mts} +161 -46
- package/dist/phone-protocol.d.mts +20 -5
- package/dist/{protocol-B0KcaUee.d.mts → protocol-BArCKISQ.d.mts} +60 -11
- package/dist/protocol.d.mts +2 -2
- package/dist/protocol.mjs +11 -3
- package/dist/receive-protocol.mjs +3838 -3343
- package/dist/state.d.mts +2 -2
- package/dist/state.mjs +304 -80
- package/package.json +1 -1
package/dist/state.mjs
CHANGED
|
@@ -39,20 +39,21 @@ function parseAgentEvents(lines) {
|
|
|
39
39
|
const { kind, command, summary } = entry;
|
|
40
40
|
const requestId = typeof entry.requestId === "string" ? entry.requestId : null;
|
|
41
41
|
if (kind === "request.started" && requestId && Number.isFinite(ts)) started.push([requestId, ts]);
|
|
42
|
-
|
|
42
|
+
const scopeChange = kind === "request.started" && (command === "open" || command === "close");
|
|
43
|
+
if (kind !== "action.recorded" && kind !== "request.finished" && !scopeChange) continue;
|
|
43
44
|
const failed = kind === "request.finished" && entry.status === "error";
|
|
44
45
|
if (kind === "request.finished" && !failed) {
|
|
45
46
|
if (requestId) finished.push(requestId);
|
|
46
47
|
continue;
|
|
47
48
|
}
|
|
48
49
|
if (!Number.isFinite(ts) || typeof entry.session !== "string" || typeof command !== "string") continue;
|
|
49
|
-
if (typeof summary !== "string") continue;
|
|
50
|
+
if (typeof summary !== "string" && !scopeChange) continue;
|
|
50
51
|
events.push({
|
|
51
52
|
ts,
|
|
52
53
|
session: entry.session,
|
|
53
54
|
kind,
|
|
54
55
|
command,
|
|
55
|
-
summary,
|
|
56
|
+
summary: typeof summary === "string" ? summary : "",
|
|
56
57
|
failed,
|
|
57
58
|
details: object$1(entry.details),
|
|
58
59
|
requestId
|
|
@@ -147,11 +148,14 @@ function readCompleteLines(path, start) {
|
|
|
147
148
|
let fd;
|
|
148
149
|
try {
|
|
149
150
|
fd = openSync(path, "r");
|
|
150
|
-
const
|
|
151
|
+
const stat = fstatSync(fd);
|
|
152
|
+
const size = stat.size;
|
|
153
|
+
const identity = `${stat.dev}:${stat.ino}`;
|
|
151
154
|
if (size <= start) return {
|
|
152
155
|
text: "",
|
|
153
156
|
next: start,
|
|
154
|
-
size
|
|
157
|
+
size,
|
|
158
|
+
identity
|
|
155
159
|
};
|
|
156
160
|
const buffer = Buffer.alloc(size - start);
|
|
157
161
|
const read = readSync(fd, buffer, 0, buffer.length, start);
|
|
@@ -159,7 +163,8 @@ function readCompleteLines(path, start) {
|
|
|
159
163
|
return {
|
|
160
164
|
text: end < 0 ? "" : buffer.toString("utf8", 0, end + 1),
|
|
161
165
|
next: start + end + 1,
|
|
162
|
-
size
|
|
166
|
+
size,
|
|
167
|
+
identity
|
|
163
168
|
};
|
|
164
169
|
} catch {
|
|
165
170
|
return null;
|
|
@@ -167,16 +172,23 @@ function readCompleteLines(path, start) {
|
|
|
167
172
|
if (fd !== void 0) closeSync(fd);
|
|
168
173
|
}
|
|
169
174
|
}
|
|
175
|
+
function trackMacosOpen(cursor, event, unknownVersion) {
|
|
176
|
+
const flags = object$1(event.details?.flags);
|
|
177
|
+
if (!event.failed) cursor.macos = event.details?.platform === "macos" || flags?.platform === "macos";
|
|
178
|
+
cursor.bundleId = cursor.macos && !event.failed && !unknownVersion && flags?.surface === "app" && typeof event.details?.appBundleId === "string" ? event.details.appBundleId : null;
|
|
179
|
+
}
|
|
170
180
|
/**
|
|
171
181
|
* Reads agent-device session actions incrementally from the supplied sessions directories. With targets,
|
|
172
182
|
* records are restricted to those devices; without targets, retained session metadata supplies attribution.
|
|
173
183
|
* Missing directories and unreadable files yield no records.
|
|
174
184
|
*/
|
|
175
|
-
function createAgentActionReader({ sessionsDirs, targets, sinceTs = -Infinity, now = Date.now, claimedDevice = () => null }) {
|
|
185
|
+
function createAgentActionReader({ sessionsDirs, targets: resolveTargets, sinceTs = -Infinity, now = Date.now, claimedDevice = () => null }) {
|
|
176
186
|
const cursors = /* @__PURE__ */ new Map();
|
|
177
|
-
const byId = new Map(targets?.map((target) => [target.id, target]));
|
|
178
187
|
return () => {
|
|
188
|
+
const targets = typeof resolveTargets === "function" ? resolveTargets() : resolveTargets;
|
|
179
189
|
if (targets && !targets.length) return [];
|
|
190
|
+
const byId = new Map(targets?.map((target) => [target.id, target]));
|
|
191
|
+
const macosByBundle = new Map(targets?.flatMap((target) => target.platform === "macos" ? [[target.bundleId, target]] : []));
|
|
180
192
|
const sessionDirs = (typeof sessionsDirs === "function" ? sessionsDirs() : sessionsDirs).flatMap((sessionsDir) => {
|
|
181
193
|
try {
|
|
182
194
|
return readdirSync(sessionsDir).map((name) => ({
|
|
@@ -205,7 +217,10 @@ function createAgentActionReader({ sessionsDirs, targets, sinceTs = -Infinity, n
|
|
|
205
217
|
runner: null,
|
|
206
218
|
session: null,
|
|
207
219
|
unknownReported: false,
|
|
208
|
-
started: /* @__PURE__ */ new Map()
|
|
220
|
+
started: /* @__PURE__ */ new Map(),
|
|
221
|
+
macos: false,
|
|
222
|
+
bundleId: null,
|
|
223
|
+
closing: null
|
|
209
224
|
};
|
|
210
225
|
cursors.set(dir, cursor);
|
|
211
226
|
try {
|
|
@@ -213,8 +228,12 @@ function createAgentActionReader({ sessionsDirs, targets, sinceTs = -Infinity, n
|
|
|
213
228
|
} catch {}
|
|
214
229
|
}
|
|
215
230
|
let chunk = readCompleteLines(eventsPath, cursor.offset);
|
|
216
|
-
if (chunk && chunk.size < cursor.offset
|
|
231
|
+
if (chunk && (chunk.size < cursor.offset || cursor.identity && chunk.identity !== cursor.identity)) {
|
|
232
|
+
cursor.bundleId = null;
|
|
233
|
+
chunk = readCompleteLines(eventsPath, 0);
|
|
234
|
+
}
|
|
217
235
|
if (!chunk) continue;
|
|
236
|
+
cursor.identity = chunk.identity;
|
|
218
237
|
cursor.offset = chunk.next;
|
|
219
238
|
lines.push(...chunk.text.split("\n"));
|
|
220
239
|
const parsed = parseAgentEvents(lines);
|
|
@@ -241,7 +260,7 @@ function createAgentActionReader({ sessionsDirs, targets, sinceTs = -Infinity, n
|
|
|
241
260
|
spans: parseRunnerSpans(text)
|
|
242
261
|
};
|
|
243
262
|
}
|
|
244
|
-
for (const event of parsed.events) if (event.command === "close" &&
|
|
263
|
+
for (const event of parsed.events) if (event.command === "close" && event.kind === "action.recorded") cursor.closes.push(event.ts);
|
|
245
264
|
const spans = sessionSpans(cursor.runner.spans, cursor.closes);
|
|
246
265
|
const deviceAt = (ts) => spans.length ? spanDevice(spans, ts) : session && claimedDevice(session, sessionsDir);
|
|
247
266
|
cursor.platform ??= parsed.events.map(platformOf).find((platform) => platform !== void 0);
|
|
@@ -252,13 +271,30 @@ function createAgentActionReader({ sessionsDirs, targets, sinceTs = -Infinity, n
|
|
|
252
271
|
});
|
|
253
272
|
if (parsed.unknownVersion && !cursor.unknownReported) {
|
|
254
273
|
const deviceId = spans.at(-1)?.deviceId ?? (session && claimedDevice(session, sessionsDir));
|
|
255
|
-
const target = targets ?
|
|
274
|
+
const target = !targets ? retainedTarget() : cursor.macos ? cursor.bundleId ? macosByBundle.get(cursor.bundleId) : void 0 : deviceId ? byId.get(deviceId) : void 0;
|
|
256
275
|
if (target) {
|
|
257
276
|
cursor.unknownReported = true;
|
|
258
277
|
out.push(formatUnknownRecord(session ?? name, parsed.unknownVersion, target, now()));
|
|
259
278
|
}
|
|
260
279
|
}
|
|
280
|
+
if (parsed.unknownVersion) cursor.bundleId = null;
|
|
261
281
|
for (const event of parsed.events) {
|
|
282
|
+
if (event.kind === "request.started") {
|
|
283
|
+
cursor.closing = event.command === "close" ? cursor.bundleId : null;
|
|
284
|
+
cursor.bundleId = null;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (targets && event.command === "open") trackMacosOpen(cursor, event, Boolean(parsed.unknownVersion));
|
|
288
|
+
if (targets && cursor.macos) {
|
|
289
|
+
if (event.command === "screenshot") continue;
|
|
290
|
+
const bundleId = cursor.bundleId ?? (event.command === "close" ? cursor.closing : null);
|
|
291
|
+
cursor.closing = null;
|
|
292
|
+
const target = bundleId ? macosByBundle.get(bundleId) : void 0;
|
|
293
|
+
const startedAt = event.requestId ? matched.get(event.requestId) : void 0;
|
|
294
|
+
if (target?.platform === "macos" && event.ts >= Math.max(sinceTs, target.launchedAt) && (startedAt ?? event.ts) >= target.launchedAt) out.push(agentRecord(event, target.id, target, startedAt));
|
|
295
|
+
if (event.command === "close") cursor.bundleId = null;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
262
298
|
if (event.ts < sinceTs) continue;
|
|
263
299
|
if (!targets) cursor.platform = platformOf(event) ?? cursor.platform;
|
|
264
300
|
const deviceId = !targets && cursor.platform === "android" ? null : deviceAt(event.ts);
|
|
@@ -452,14 +488,14 @@ function agentSessionsCacheFile() {
|
|
|
452
488
|
function diskUsageCacheDir() {
|
|
453
489
|
return join(configDir(), "disk-usage");
|
|
454
490
|
}
|
|
455
|
-
/** The
|
|
491
|
+
/** The remote Macs this Mac paired with, each with its pinned tailnet node and device token. */
|
|
456
492
|
function buildMachinesFile() {
|
|
457
493
|
return join(configDir(), "build-machines.json");
|
|
458
494
|
}
|
|
459
495
|
function buildMachinesLock() {
|
|
460
496
|
return join(configDir(), "build-machines.lock");
|
|
461
497
|
}
|
|
462
|
-
/** The
|
|
498
|
+
/** The remote Macs this Mac asked for access, separate from build credentials. */
|
|
463
499
|
function deviceHostMachinesFile() {
|
|
464
500
|
return join(configDir(), "device-host-machines.json");
|
|
465
501
|
}
|
|
@@ -539,9 +575,9 @@ function configCorrupt(reason, path = getConfigPath()) {
|
|
|
539
575
|
corrupt.code = "STIM_CONFIG_CORRUPT";
|
|
540
576
|
return corrupt;
|
|
541
577
|
}
|
|
542
|
-
/** The build worker root: `
|
|
578
|
+
/** The build worker root: `server.workerRoot` when it is absolute, else `$STIM_HOME/build-worker`. */
|
|
543
579
|
function buildWorkerRoot(config = loadConfig()) {
|
|
544
|
-
const configured = config?.
|
|
580
|
+
const configured = config?.server?.workerRoot;
|
|
545
581
|
return typeof configured === "string" && configured.startsWith("/") ? configured : join(configDir(), "build-worker");
|
|
546
582
|
}
|
|
547
583
|
function loadConfig() {
|
|
@@ -591,6 +627,46 @@ function getConcurrencyLimits({ env = process.env } = {}) {
|
|
|
591
627
|
maxDevices: resolveLimit(env.STIM_MAX_DEVICES, c.maxDevices)
|
|
592
628
|
};
|
|
593
629
|
}
|
|
630
|
+
const MAX_SETTING = {
|
|
631
|
+
ios: {
|
|
632
|
+
key: "iosParkedMax",
|
|
633
|
+
env: "STIM_POOL_IOS_PARKED_MAX"
|
|
634
|
+
},
|
|
635
|
+
android: {
|
|
636
|
+
key: "androidParkedMax",
|
|
637
|
+
env: "STIM_POOL_ANDROID_PARKED_MAX"
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
function parseMax(raw, strings) {
|
|
641
|
+
const value = strings && typeof raw === "string" ? /^\d+$/.test(raw.trim()) ? Number(raw.trim()) : NaN : raw;
|
|
642
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) return null;
|
|
643
|
+
return value;
|
|
644
|
+
}
|
|
645
|
+
function getParkedMax(platform, { config, env = process.env } = {}) {
|
|
646
|
+
const { key, env: envKey } = MAX_SETTING[platform];
|
|
647
|
+
const fromEnv = env[envKey];
|
|
648
|
+
const explicit = fromEnv !== void 0 && fromEnv !== "";
|
|
649
|
+
const pool = (config === void 0 ? loadConfig() : config)?.pool;
|
|
650
|
+
if (!explicit && pool !== void 0 && (pool === null || typeof pool !== "object" || Array.isArray(pool))) return {
|
|
651
|
+
max: 0,
|
|
652
|
+
error: "Invalid pool value. Expected an object with simulator and emulator bounds."
|
|
653
|
+
};
|
|
654
|
+
const fromConfig = pool !== null && typeof pool === "object" && !Array.isArray(pool) ? pool[key] : void 0;
|
|
655
|
+
const raw = explicit ? fromEnv : fromConfig;
|
|
656
|
+
if (raw === void 0) return {
|
|
657
|
+
max: env.STIM_HOME ? 0 : 3,
|
|
658
|
+
error: null
|
|
659
|
+
};
|
|
660
|
+
const parsed = parseMax(raw, explicit);
|
|
661
|
+
if (parsed === null) return {
|
|
662
|
+
max: 0,
|
|
663
|
+
error: `Invalid ${explicit ? envKey : `pool.${key}`} value ${JSON.stringify(raw)}. Expected a whole number of parked ${platform === "ios" ? "simulators" : "emulators"}, 0 or more.`
|
|
664
|
+
};
|
|
665
|
+
return {
|
|
666
|
+
max: explicit ? parsed : env.STIM_HOME ? 0 : parsed,
|
|
667
|
+
error: null
|
|
668
|
+
};
|
|
669
|
+
}
|
|
594
670
|
function resolveLimit(envVal, cfgVal) {
|
|
595
671
|
const raw = envVal !== void 0 && envVal !== null && envVal !== "" ? Number(envVal) : typeof cfgVal === "number" ? cfgVal : NaN;
|
|
596
672
|
if (!Number.isFinite(raw) || raw <= 0) return 0;
|
|
@@ -735,11 +811,11 @@ const AGENT_PATH = /^\/device-host\/agent\/[a-f0-9-]{36}\/$/;
|
|
|
735
811
|
const LEASE_NAME = /^[A-Za-z0-9._-]{1,128}$/;
|
|
736
812
|
const LEASE_DEVICE_KEY = /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)+@[1-9][0-9]{0,9}$/;
|
|
737
813
|
function parseHostedAgentLease(value) {
|
|
738
|
-
if (!isJsonObject(value) || Object.keys(value).length !== (value.backend === "ios-instance" ? 5 : 4) || ![
|
|
814
|
+
if (!isJsonObject(value) || Object.keys(value).length !== (value.backend === "ios-instance" || value.backend === "android-instance" ? 5 : 4) || ![
|
|
739
815
|
value.tenant,
|
|
740
816
|
value.runId,
|
|
741
817
|
value.clientId
|
|
742
|
-
].every((name) => typeof name === "string" && LEASE_NAME.test(name)) || typeof value.deviceKey !== "string" || !(value.backend === "ios-instance" ? /^ios:mobile:[a-fA-F0-9-]{36}$/.test(value.deviceKey) : value.backend === void 0 && LEASE_DEVICE_KEY.test(value.deviceKey))) return null;
|
|
818
|
+
].every((name) => typeof name === "string" && LEASE_NAME.test(name)) || typeof value.deviceKey !== "string" || !(value.backend === "ios-instance" ? /^ios:mobile:[a-fA-F0-9-]{36}$/.test(value.deviceKey) : value.backend === "android-instance" ? /^android:mobile:emulator-[0-9]+$/.test(value.deviceKey) : value.backend === void 0 && LEASE_DEVICE_KEY.test(value.deviceKey))) return null;
|
|
743
819
|
return value;
|
|
744
820
|
}
|
|
745
821
|
/** A grant naming a driver this client does not know is not usable; it parses as null. */
|
|
@@ -852,10 +928,6 @@ const RELATIVE_PATH = {
|
|
|
852
928
|
};
|
|
853
929
|
const BOOLEAN = { kind: "boolean" };
|
|
854
930
|
const OBJECT = { kind: "object" };
|
|
855
|
-
const REMOTE = {
|
|
856
|
-
kind: "choice",
|
|
857
|
-
choices: REMOTE_DEVICE_BACKENDS
|
|
858
|
-
};
|
|
859
931
|
const PARKED_MAX = {
|
|
860
932
|
kind: "number",
|
|
861
933
|
integer: true,
|
|
@@ -912,7 +984,7 @@ const SETTINGS = [
|
|
|
912
984
|
default: "auto"
|
|
913
985
|
},
|
|
914
986
|
optimization("android.gradleBuildCache", "Gradle build cache"),
|
|
915
|
-
optimization("android.targetAbiOnly", "Narrow Debug builds to the device ABI"),
|
|
987
|
+
optimization("android.targetAbiOnly", "Narrow Debug and hosted Android builds to the device ABI"),
|
|
916
988
|
{
|
|
917
989
|
key: "ios.deviceType",
|
|
918
990
|
type: STRING,
|
|
@@ -939,8 +1011,8 @@ const SETTINGS = [
|
|
|
939
1011
|
pattern: TAILNET_MACHINE_PATTERN,
|
|
940
1012
|
patternHelp: "eas, proxy, auto, or a tailnet machine name"
|
|
941
1013
|
},
|
|
942
|
-
scopes:
|
|
943
|
-
description: "Default iOS remote target: eas, proxy, or an approved Mac in
|
|
1014
|
+
scopes: EVERY,
|
|
1015
|
+
description: "Default iOS remote target: eas, proxy, or an approved Mac in remote.machines; auto places on an approved Mac when this Mac is full or busy"
|
|
944
1016
|
},
|
|
945
1017
|
{
|
|
946
1018
|
key: "ios.simslimProfile",
|
|
@@ -1032,9 +1104,13 @@ const SETTINGS = [
|
|
|
1032
1104
|
},
|
|
1033
1105
|
{
|
|
1034
1106
|
key: "android.remote",
|
|
1035
|
-
type:
|
|
1036
|
-
|
|
1037
|
-
|
|
1107
|
+
type: {
|
|
1108
|
+
kind: "string",
|
|
1109
|
+
pattern: TAILNET_MACHINE_PATTERN,
|
|
1110
|
+
patternHelp: "eas, proxy, auto, or a tailnet machine name"
|
|
1111
|
+
},
|
|
1112
|
+
scopes: EVERY,
|
|
1113
|
+
description: "Default Android remote target: eas, proxy, or an approved Mac in remote.machines; auto places on an approved Mac when this Mac is full or busy"
|
|
1038
1114
|
},
|
|
1039
1115
|
{
|
|
1040
1116
|
key: "metro.tunnel",
|
|
@@ -1101,9 +1177,20 @@ const SETTINGS = [
|
|
|
1101
1177
|
minimum: 0
|
|
1102
1178
|
},
|
|
1103
1179
|
scopes: EVERY,
|
|
1104
|
-
default:
|
|
1180
|
+
default: 30,
|
|
1105
1181
|
description: "Minutes an owned simulator or emulator stays idle before the workspace supervisor shuts it down; 0 never"
|
|
1106
1182
|
},
|
|
1183
|
+
{
|
|
1184
|
+
key: "devices.reclaimIdleMinutes",
|
|
1185
|
+
type: {
|
|
1186
|
+
kind: "number",
|
|
1187
|
+
integer: true,
|
|
1188
|
+
minimum: 0
|
|
1189
|
+
},
|
|
1190
|
+
scopes: EVERY,
|
|
1191
|
+
default: 10,
|
|
1192
|
+
description: "Minutes an owned simulator or emulator stays idle before a waiting run reclaims its device slot; 0 never"
|
|
1193
|
+
},
|
|
1107
1194
|
{
|
|
1108
1195
|
key: "macos.product",
|
|
1109
1196
|
type: {
|
|
@@ -1543,12 +1630,6 @@ const SETTINGS = [
|
|
|
1543
1630
|
scopedHomeValue: 0,
|
|
1544
1631
|
description: "Parked emulators kept for adoption; 0 turns parking off"
|
|
1545
1632
|
},
|
|
1546
|
-
{
|
|
1547
|
-
key: "hosting.machines",
|
|
1548
|
-
type: { kind: "strings" },
|
|
1549
|
-
scopes: MACHINE,
|
|
1550
|
-
description: "Tailscale names of the Macs that may host owned simulator sessions for this one, each optionally with :<port> of its tailscale serve route (default 7443); doctor --fix asks for separate device-host approval"
|
|
1551
|
-
},
|
|
1552
1633
|
{
|
|
1553
1634
|
key: "hosting.agentDriver",
|
|
1554
1635
|
type: {
|
|
@@ -1557,7 +1638,7 @@ const SETTINGS = [
|
|
|
1557
1638
|
},
|
|
1558
1639
|
scopes: MACHINE,
|
|
1559
1640
|
default: "none",
|
|
1560
|
-
description: "Tool this Mac starts so a client's coding agent can drive the macOS apps
|
|
1641
|
+
description: "Tool this Mac starts so a client's coding agent can drive the macOS apps, iOS simulators and Android emulators it hosts for that client; none starts nothing"
|
|
1561
1642
|
},
|
|
1562
1643
|
{
|
|
1563
1644
|
key: "server.acceptClientBuilds",
|
|
@@ -1567,13 +1648,13 @@ const SETTINGS = [
|
|
|
1567
1648
|
description: "Whether a Mac approved for builds or device hosting here may update this Mac's stim-server service to that Mac's own stim-server build; releases from npm need no setting"
|
|
1568
1649
|
},
|
|
1569
1650
|
{
|
|
1570
|
-
key: "
|
|
1651
|
+
key: "remote.machines",
|
|
1571
1652
|
type: { kind: "strings" },
|
|
1572
1653
|
scopes: MACHINE,
|
|
1573
|
-
description: "Tailscale names of the Macs
|
|
1654
|
+
description: "Tailscale names of the remote Macs this Mac may build on and host owned simulators on, each optionally with :<port> of its tailscale serve route (default 7443); what each is used for follows the build and device-host approvals it grants, which doctor --fix asks for separately"
|
|
1574
1655
|
},
|
|
1575
1656
|
{
|
|
1576
|
-
key: "
|
|
1657
|
+
key: "remote.build",
|
|
1577
1658
|
type: {
|
|
1578
1659
|
kind: "string",
|
|
1579
1660
|
pattern: TAILNET_MACHINE_PATTERN,
|
|
@@ -1581,38 +1662,38 @@ const SETTINGS = [
|
|
|
1581
1662
|
},
|
|
1582
1663
|
scopes: MACHINE,
|
|
1583
1664
|
default: "auto",
|
|
1584
|
-
env: "
|
|
1585
|
-
description: "Build placement: auto follows
|
|
1665
|
+
env: "STIM_REMOTE_BUILD",
|
|
1666
|
+
description: "Build placement: auto follows remote.buildMode with local fallback; local always builds here; a name in remote.machines requires that remote Mac without fallback. The ios, android and macos --remote-build flag overrides this setting and its environment override"
|
|
1586
1667
|
},
|
|
1587
1668
|
{
|
|
1588
|
-
key: "
|
|
1669
|
+
key: "remote.buildMode",
|
|
1589
1670
|
type: {
|
|
1590
1671
|
kind: "choice",
|
|
1591
1672
|
choices: OFFLOAD_MODES
|
|
1592
1673
|
},
|
|
1593
1674
|
scopes: MACHINE,
|
|
1594
1675
|
default: "auto",
|
|
1595
|
-
env: "
|
|
1596
|
-
description: "When
|
|
1676
|
+
env: "STIM_REMOTE_BUILD_MODE",
|
|
1677
|
+
description: "When remote.build is auto, where iOS simulator Debug, Android emulator debug and macOS SwiftPM Debug builds run: auto builds here while this Mac has a free concurrency.maxBuilds slot and its load is under server.maxLoadPerCore, and otherwise builds on a less loaded Mac in remote.machines; force builds on a remote Mac whenever one can take the build; off always builds here"
|
|
1597
1678
|
},
|
|
1598
1679
|
{
|
|
1599
|
-
key: "
|
|
1680
|
+
key: "server.maxLoadPerCore",
|
|
1600
1681
|
type: {
|
|
1601
1682
|
kind: "number",
|
|
1602
1683
|
minimum: .1
|
|
1603
1684
|
},
|
|
1604
1685
|
scopes: MACHINE,
|
|
1605
1686
|
default: 2,
|
|
1606
|
-
description: "Load per core (5-minute load average divided by the CPU count) at which
|
|
1687
|
+
description: "Load per core (5-minute load average divided by the CPU count) at which this Mac counts as saturated: it declines builds other Macs send it, automatic build placement stops preferring it, and automatic iOS and Android device placement treats it as busy"
|
|
1607
1688
|
},
|
|
1608
1689
|
{
|
|
1609
|
-
key: "
|
|
1690
|
+
key: "server.workerRoot",
|
|
1610
1691
|
type: ABSOLUTE_PATH,
|
|
1611
1692
|
scopes: MACHINE,
|
|
1612
|
-
description: "Directory where stim-server keeps the checkouts, dependencies and build state of Macs that build here; default $STIM_HOME/build-worker"
|
|
1693
|
+
description: "Directory where stim-server keeps the checkouts, dependencies and build state of the Macs that build here; default $STIM_HOME/build-worker"
|
|
1613
1694
|
},
|
|
1614
1695
|
{
|
|
1615
|
-
key: "
|
|
1696
|
+
key: "server.gradleDaemonIdleMinutes",
|
|
1616
1697
|
type: {
|
|
1617
1698
|
kind: "number",
|
|
1618
1699
|
integer: true,
|
|
@@ -1621,7 +1702,7 @@ const SETTINGS = [
|
|
|
1621
1702
|
},
|
|
1622
1703
|
scopes: MACHINE,
|
|
1623
1704
|
default: 30,
|
|
1624
|
-
description: "Minutes the Gradle daemon of an
|
|
1705
|
+
description: "Minutes the Gradle daemon of an Android build another Mac sent here stays warm after the build; 0 stops it when the build ends"
|
|
1625
1706
|
},
|
|
1626
1707
|
{
|
|
1627
1708
|
key: "caches.buildCache",
|
|
@@ -1791,7 +1872,7 @@ function tryAcquireBuildSlotClaim({ max, details }) {
|
|
|
1791
1872
|
if (refusal && !busy) throw refusal;
|
|
1792
1873
|
return null;
|
|
1793
1874
|
}
|
|
1794
|
-
/** Build slots held for a
|
|
1875
|
+
/** Build slots held for a remote Mac's offloaded builds, which run under another Stim home. */
|
|
1795
1876
|
function liveOffloadedSlots() {
|
|
1796
1877
|
let names;
|
|
1797
1878
|
try {
|
|
@@ -1809,9 +1890,9 @@ function liveOffloadedSlots() {
|
|
|
1809
1890
|
return count;
|
|
1810
1891
|
}
|
|
1811
1892
|
function maxLoadPerCore() {
|
|
1812
|
-
const value = loadConfig()?.
|
|
1893
|
+
const value = loadConfig()?.server?.maxLoadPerCore;
|
|
1813
1894
|
if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
|
|
1814
|
-
return settingDefinition("
|
|
1895
|
+
return settingDefinition("server.maxLoadPerCore").default;
|
|
1815
1896
|
}
|
|
1816
1897
|
function liveNativeBuilds() {
|
|
1817
1898
|
const dir = join(configDir(), "workspaces");
|
|
@@ -2594,7 +2675,7 @@ function listSegments(dir, closedOnly = false) {
|
|
|
2594
2675
|
return segments.toSorted((a, b) => a.start - b.start);
|
|
2595
2676
|
}
|
|
2596
2677
|
/** Every device slot recorded under a workspace's recordings directory. */
|
|
2597
|
-
function listRecordedDevices(recordingsDir) {
|
|
2678
|
+
function listRecordedDevices(recordingsDir, closedOnly = false) {
|
|
2598
2679
|
return listDir(recordingsDir).flatMap((name) => {
|
|
2599
2680
|
const match = DEVICE.exec(name);
|
|
2600
2681
|
if (!match) return [];
|
|
@@ -2603,7 +2684,7 @@ function listRecordedDevices(recordingsDir) {
|
|
|
2603
2684
|
platform: match[1],
|
|
2604
2685
|
slot: match[2],
|
|
2605
2686
|
dir,
|
|
2606
|
-
segments: listSegments(dir)
|
|
2687
|
+
segments: listSegments(dir, closedOnly)
|
|
2607
2688
|
}];
|
|
2608
2689
|
});
|
|
2609
2690
|
}
|
|
@@ -2840,6 +2921,8 @@ const WORKSPACE_PHASES = [
|
|
|
2840
2921
|
"live",
|
|
2841
2922
|
"idle"
|
|
2842
2923
|
];
|
|
2924
|
+
/** The tutorial format version the installed Stim writes into the tutorial app. */
|
|
2925
|
+
const TUTORIAL_VERSION = 1;
|
|
2843
2926
|
/**
|
|
2844
2927
|
* Where a memory figure comes from. `footprint` is each process's physical footprint, which Activity Monitor's Memory
|
|
2845
2928
|
* column shows. `rss` sums resident set sizes, which count pages shared between processes once per process, so a
|
|
@@ -3131,7 +3214,7 @@ function readBuildDetail(root, claimId) {
|
|
|
3131
3214
|
if (typeof raw.updatedAt !== "string") return null;
|
|
3132
3215
|
return {
|
|
3133
3216
|
step: NATIVE_BUILD_STEPS.includes(raw.step) ? raw.step : null,
|
|
3134
|
-
unit: raw.unit === "targets" || raw.unit === "tasks" ? raw.unit : null,
|
|
3217
|
+
unit: raw.unit === "targets" || raw.unit === "tasks" || raw.unit === "steps" ? raw.unit : null,
|
|
3135
3218
|
done: countOrNull(raw.done),
|
|
3136
3219
|
total: countOrNull(raw.total),
|
|
3137
3220
|
line: typeof raw.line === "string" ? raw.line : null,
|
|
@@ -3197,7 +3280,7 @@ function readAgentSessionsCache() {
|
|
|
3197
3280
|
//#region state/stim-build.ts
|
|
3198
3281
|
/**
|
|
3199
3282
|
* A digest of Stim's built code: the first 16 hex characters of the sha256 over the sorted `*.mjs` names and
|
|
3200
|
-
* bytes of a `stim` `dist` directory. A client and a
|
|
3283
|
+
* bytes of a `stim` `dist` directory. A client and a remote Mac offload only when both compute the same one.
|
|
3201
3284
|
*/
|
|
3202
3285
|
function stimBuildDigest(dir) {
|
|
3203
3286
|
try {
|
|
@@ -3225,6 +3308,7 @@ function readIdleStop(state) {
|
|
|
3225
3308
|
idleMinutes: record.idleMinutes
|
|
3226
3309
|
};
|
|
3227
3310
|
}
|
|
3311
|
+
const DEVICE_RECLAIM_REASON = "reclaimed for a waiting run";
|
|
3228
3312
|
const DEVICE_IDLE_SHUTDOWN_KEY = "deviceIdleShutdowns";
|
|
3229
3313
|
/** The recorded idle shutdowns by device slot key (`ios`, `android`, `ios:<slot>`). */
|
|
3230
3314
|
function readDeviceIdleShutdowns(state) {
|
|
@@ -3235,7 +3319,8 @@ function readDeviceIdleShutdowns(state) {
|
|
|
3235
3319
|
const record = value;
|
|
3236
3320
|
if (typeof record?.at === "string" && typeof record.idleMinutes === "number") found[key] = {
|
|
3237
3321
|
at: record.at,
|
|
3238
|
-
idleMinutes: record.idleMinutes
|
|
3322
|
+
idleMinutes: record.idleMinutes,
|
|
3323
|
+
...typeof record.reason === "string" ? { reason: record.reason } : {}
|
|
3239
3324
|
};
|
|
3240
3325
|
}
|
|
3241
3326
|
return found;
|
|
@@ -3354,6 +3439,22 @@ function buildDiagnostics(value) {
|
|
|
3354
3439
|
}];
|
|
3355
3440
|
}).toSorted((a, b) => Number(b.file !== null) - Number(a.file !== null)).slice(0, DIAGNOSTIC_CAP);
|
|
3356
3441
|
}
|
|
3442
|
+
function devicePlacement(value) {
|
|
3443
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
3444
|
+
const record = value;
|
|
3445
|
+
if (record.decision !== "local" && record.decision !== "hosted" && record.decision !== "waited-locally" || typeof record.reason !== "string") return null;
|
|
3446
|
+
return {
|
|
3447
|
+
decision: record.decision,
|
|
3448
|
+
reason: record.reason,
|
|
3449
|
+
...typeof record.machine === "string" ? { machine: record.machine } : {}
|
|
3450
|
+
};
|
|
3451
|
+
}
|
|
3452
|
+
function readDevicePlacement(state, platform, slot = "default") {
|
|
3453
|
+
const slots = isJsonObject(state?.deviceSlots) ? state.deviceSlots : {};
|
|
3454
|
+
const saved = isJsonObject(slots[slot]) ? slots[slot] : {};
|
|
3455
|
+
const device = slot === "default" ? state?.[platform] : saved[platform];
|
|
3456
|
+
return devicePlacement(isJsonObject(device) ? device.devicePlacement : void 0);
|
|
3457
|
+
}
|
|
3357
3458
|
function lastBuildReport(platform, value) {
|
|
3358
3459
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
3359
3460
|
const record = value;
|
|
@@ -3363,6 +3464,7 @@ function lastBuildReport(platform, value) {
|
|
|
3363
3464
|
const finished = new Date(Date.parse(record.startedAt) + (durationMs ?? NaN));
|
|
3364
3465
|
const reason = record.cacheHit === "local" || record.cacheHit === "remote" ? null : parseMissReason(record.missReason);
|
|
3365
3466
|
const diagnostics = record.status === "failed" ? buildDiagnostics(record.diagnostics) : [];
|
|
3467
|
+
const placed = devicePlacement(record.devicePlacement);
|
|
3366
3468
|
return {
|
|
3367
3469
|
platform,
|
|
3368
3470
|
status: record.status,
|
|
@@ -3374,6 +3476,7 @@ function lastBuildReport(platform, value) {
|
|
|
3374
3476
|
finishedAt: Number.isNaN(finished.getTime()) ? null : finished.toISOString(),
|
|
3375
3477
|
...typeof record.errorCode === "string" ? { errorCode: record.errorCode } : {},
|
|
3376
3478
|
...reason ? { missReason: reason } : {},
|
|
3479
|
+
...placed ? { devicePlacement: placed } : {},
|
|
3377
3480
|
...typeof record.buildMachine === "string" ? { buildMachine: record.buildMachine } : {},
|
|
3378
3481
|
...typeof record.builtOn === "string" ? { builtOn: record.builtOn } : {},
|
|
3379
3482
|
...typeof record.offloadedTo === "string" ? { offloadedTo: record.offloadedTo } : {},
|
|
@@ -3416,7 +3519,11 @@ function historyEntry(platform, value) {
|
|
|
3416
3519
|
function readBuildHistory(state) {
|
|
3417
3520
|
const history = state?.[BUILD_HISTORY_KEY];
|
|
3418
3521
|
const builds = {};
|
|
3419
|
-
for (const platform of [
|
|
3522
|
+
for (const platform of [
|
|
3523
|
+
"ios",
|
|
3524
|
+
"android",
|
|
3525
|
+
"macos"
|
|
3526
|
+
]) {
|
|
3420
3527
|
const list = history?.[platform];
|
|
3421
3528
|
if (!Array.isArray(list)) continue;
|
|
3422
3529
|
const entries = list.slice(0, 10).map((value) => historyEntry(platform, value)).filter((entry) => entry !== null);
|
|
@@ -3513,12 +3620,21 @@ function readViewedDevices(dir = deviceViewersDir()) {
|
|
|
3513
3620
|
//#endregion
|
|
3514
3621
|
//#region state/stats.ts
|
|
3515
3622
|
const STATS_VERSION = 1;
|
|
3516
|
-
/** Where a compiling build ran: here, on a
|
|
3623
|
+
/** Where a compiling build ran: here, on a remote Mac, or here after offloading it failed. */
|
|
3517
3624
|
const PLACEMENT_DECISIONS = [
|
|
3518
3625
|
"here",
|
|
3519
3626
|
"offloaded",
|
|
3520
3627
|
"fell-back"
|
|
3521
3628
|
];
|
|
3629
|
+
const CAPACITY_REFUSAL_LIMIT = 50;
|
|
3630
|
+
const CAPACITY_REFUSAL_MAX_AGE_MS = 6048e5;
|
|
3631
|
+
function trimCapacityRefusals(events, now) {
|
|
3632
|
+
return events.filter((event) => {
|
|
3633
|
+
const age = now - Date.parse(event.at);
|
|
3634
|
+
return age >= 0 && age <= 6048e5;
|
|
3635
|
+
}).toSorted((a, b) => Date.parse(a.at) - Date.parse(b.at)).slice(-50);
|
|
3636
|
+
}
|
|
3637
|
+
const trimCapacityWaits = trimCapacityRefusals;
|
|
3522
3638
|
const PLATFORMS = ["ios", "android"];
|
|
3523
3639
|
const OUTCOMES = ["hit", "cold"];
|
|
3524
3640
|
const HISTORY_LIMIT = 10;
|
|
@@ -3657,6 +3773,8 @@ function normalize(record) {
|
|
|
3657
3773
|
if (Object.keys(normalized).length) history[key] = normalized;
|
|
3658
3774
|
}
|
|
3659
3775
|
const placements = Array.isArray(record.placements) ? record.placements.flatMap(normalizePlacement) : [];
|
|
3776
|
+
const capacityRefusals = Array.isArray(record.capacityRefusals) ? record.capacityRefusals.flatMap(normalizeCapacityRefusal) : [];
|
|
3777
|
+
const capacityWaits = Array.isArray(record.capacityWaits) ? record.capacityWaits.flatMap(normalizeCapacityWait) : [];
|
|
3660
3778
|
const buildMachines = {};
|
|
3661
3779
|
if (isObject(record.buildMachines)) {
|
|
3662
3780
|
for (const [name, totals] of Object.entries(record.buildMachines)) if (isObject(totals)) buildMachines[name] = normalizeMachineTotals(totals);
|
|
@@ -3667,6 +3785,8 @@ function normalize(record) {
|
|
|
3667
3785
|
projects,
|
|
3668
3786
|
...Object.keys(history).length ? { history } : {},
|
|
3669
3787
|
...placements.length ? { placements } : {},
|
|
3788
|
+
...capacityRefusals.length ? { capacityRefusals } : {},
|
|
3789
|
+
...capacityWaits.length ? { capacityWaits } : {},
|
|
3670
3790
|
...Object.keys(buildMachines).length ? { buildMachines } : {}
|
|
3671
3791
|
};
|
|
3672
3792
|
}
|
|
@@ -3677,6 +3797,7 @@ function normalizePlacement(value) {
|
|
|
3677
3797
|
if (typeof reason !== "string" || typeof project !== "string") return [];
|
|
3678
3798
|
const buildMs = wholeMs(value.buildMs);
|
|
3679
3799
|
const slotWaitMs = wholeMs(value.slotWaitMs);
|
|
3800
|
+
const deviceSlotWaitMs = wholeMs(value.deviceSlotWaitMs);
|
|
3680
3801
|
const localEstimateMs = wholeMs(value.localEstimateMs);
|
|
3681
3802
|
return [{
|
|
3682
3803
|
at: timestamp(value.at),
|
|
@@ -3687,10 +3808,40 @@ function normalizePlacement(value) {
|
|
|
3687
3808
|
...typeof machine === "string" && machine !== "" ? { machine } : {},
|
|
3688
3809
|
...buildMs > 0 ? { buildMs } : {},
|
|
3689
3810
|
...slotWaitMs > 0 ? { slotWaitMs } : {},
|
|
3811
|
+
...deviceSlotWaitMs > 0 ? { deviceSlotWaitMs } : {},
|
|
3690
3812
|
...localEstimateMs > 0 ? { localEstimateMs } : {},
|
|
3691
3813
|
...value.failed === true ? { failed: true } : {}
|
|
3692
3814
|
}];
|
|
3693
3815
|
}
|
|
3816
|
+
function normalizeCapacityRefusal(value) {
|
|
3817
|
+
if (!isObject(value)) return [];
|
|
3818
|
+
const { at, kind, platform, max, workspace } = value;
|
|
3819
|
+
if (kind !== "device" || !PLATFORMS.includes(platform)) return [];
|
|
3820
|
+
if (typeof at !== "string" || !Number.isFinite(Date.parse(at))) return [];
|
|
3821
|
+
if (typeof max !== "number" || !Number.isInteger(max) || max <= 0) return [];
|
|
3822
|
+
if (typeof workspace !== "string" || workspace === "") return [];
|
|
3823
|
+
return [{
|
|
3824
|
+
at: new Date(at).toISOString(),
|
|
3825
|
+
kind,
|
|
3826
|
+
platform,
|
|
3827
|
+
max,
|
|
3828
|
+
workspace
|
|
3829
|
+
}];
|
|
3830
|
+
}
|
|
3831
|
+
function normalizeCapacityWait(value) {
|
|
3832
|
+
if (!isObject(value) || value.kind !== "device-wait") return [];
|
|
3833
|
+
const refusal = normalizeCapacityRefusal({
|
|
3834
|
+
...value,
|
|
3835
|
+
kind: "device"
|
|
3836
|
+
})[0];
|
|
3837
|
+
if (!refusal || typeof value.ms !== "number" || !Number.isFinite(value.ms) || value.ms < 0) return [];
|
|
3838
|
+
return [{
|
|
3839
|
+
...refusal,
|
|
3840
|
+
kind: "device-wait",
|
|
3841
|
+
ms: Math.round(value.ms),
|
|
3842
|
+
...typeof value.reclaimed === "number" && Number.isInteger(value.reclaimed) && value.reclaimed > 0 ? { reclaimed: value.reclaimed } : {}
|
|
3843
|
+
}];
|
|
3844
|
+
}
|
|
3694
3845
|
function normalizeMachineTotals(totals) {
|
|
3695
3846
|
const savedMs = Number(totals.savedMs);
|
|
3696
3847
|
return {
|
|
@@ -3810,6 +3961,8 @@ function decodeStats(text) {
|
|
|
3810
3961
|
function readStatsReport(key, now) {
|
|
3811
3962
|
const { record, note } = readStats();
|
|
3812
3963
|
const project = key ? record?.projects[key] : null;
|
|
3964
|
+
const capacityRefusals = trimCapacityRefusals(record?.capacityRefusals ?? [], now).toReversed();
|
|
3965
|
+
const capacityWaits = trimCapacityWaits(record?.capacityWaits ?? [], now).toReversed();
|
|
3813
3966
|
return {
|
|
3814
3967
|
report: {
|
|
3815
3968
|
version: 1,
|
|
@@ -3823,6 +3976,8 @@ function readStatsReport(key, now) {
|
|
|
3823
3976
|
android: record?.machine.android ?? null
|
|
3824
3977
|
},
|
|
3825
3978
|
offload: offloadSummary(record, now),
|
|
3979
|
+
...capacityRefusals.length ? { capacityRefusals } : {},
|
|
3980
|
+
...capacityWaits.length ? { capacityWaits } : {},
|
|
3826
3981
|
agentDevice: readAgentDeviceUsage(),
|
|
3827
3982
|
swiftpmCache: readSwiftpmCacheUsage()
|
|
3828
3983
|
},
|
|
@@ -3923,9 +4078,16 @@ function parseHostedOfferRequest(value) {
|
|
|
3923
4078
|
...typeof value.deviceProfile === "string" ? { deviceProfile: value.deviceProfile } : {}
|
|
3924
4079
|
};
|
|
3925
4080
|
}
|
|
4081
|
+
function parseHostedLocalDeviceCount(value) {
|
|
4082
|
+
if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) return value;
|
|
4083
|
+
if (isJsonObject(value) && typeof value.unknown === "string" && value.unknown) return { unknown: value.unknown.slice(0, 4e3) };
|
|
4084
|
+
return null;
|
|
4085
|
+
}
|
|
3926
4086
|
function parseHostedNativeOffer(value) {
|
|
3927
4087
|
if (!isJsonObject(value) || !isJsonObject(value.resources)) return null;
|
|
3928
4088
|
const { resources } = value;
|
|
4089
|
+
const local = parseHostedLocalDeviceCount(resources.localDevices);
|
|
4090
|
+
if (resources.localDevices !== void 0 && local === null) return null;
|
|
3929
4091
|
if (value.platform !== "ios" && value.platform !== "android" && value.platform !== "macos" || value.declined !== null && (typeof value.declined !== "string" || !value.declined || value.declined.length > 4e3) || !Number.isInteger(resources.cpus) || Number(resources.cpus) < 1 || ["loadPerCore", "memoryFreeBytes"].some((key) => typeof resources[key] !== "number" || !Number.isFinite(resources[key]) || resources[key] < 0) || resources.workerDiskFreeBytes !== null && (typeof resources.workerDiskFreeBytes !== "number" || !Number.isFinite(resources.workerDiskFreeBytes) || resources.workerDiskFreeBytes < 0) || resources.memoryPressure !== null && resources.memoryPressure !== "normal" && resources.memoryPressure !== "warning" && resources.memoryPressure !== "critical") return null;
|
|
3930
4092
|
let choice = null;
|
|
3931
4093
|
if (value.choice !== null) {
|
|
@@ -3961,7 +4123,8 @@ function parseHostedNativeOffer(value) {
|
|
|
3961
4123
|
loadPerCore: resources.loadPerCore,
|
|
3962
4124
|
memoryFreeBytes: resources.memoryFreeBytes,
|
|
3963
4125
|
memoryPressure: resources.memoryPressure,
|
|
3964
|
-
workerDiskFreeBytes: resources.workerDiskFreeBytes
|
|
4126
|
+
workerDiskFreeBytes: resources.workerDiskFreeBytes,
|
|
4127
|
+
...local !== null ? { localDevices: local } : {}
|
|
3965
4128
|
}
|
|
3966
4129
|
};
|
|
3967
4130
|
}
|
|
@@ -3991,7 +4154,7 @@ function readHostedSessions() {
|
|
|
3991
4154
|
"stopping",
|
|
3992
4155
|
"stopped",
|
|
3993
4156
|
"unknown"
|
|
3994
|
-
].includes(String(entry.state)) || entry.platform === "android" && !hostedConsolePort(entry.consolePort) || (entry.platform === "macos" ? !hostedMacosAppSlot(entry.appSlot) : entry.appSlot !== void 0) || entry.device !== null && !parseHostedPlatformDevice(entry.device, entry.platform) || entry.platform === "macos" && entry.device !== null && entry.device.appSlot !== entry.appSlot || entry.platform === "android" && entry.device !== null && entry.device.consolePort !== entry.consolePort || entry.state === "ready" && entry.device === null || entry.notice !== void 0 && typeof entry.notice !== "string" || entry.appAttempt !== void 0 && (typeof entry.appAttempt !== "string" || !/^[a-zA-Z0-9_-]{1,128}$/.test(entry.appAttempt)) || entry.metroPort !== void 0 && (typeof entry.metroPort !== "number" || !Number.isInteger(entry.metroPort) || entry.metroPort < 1 || entry.metroPort > 65535)) throw new Error(`Malformed hosted session record: ${root}`);
|
|
4157
|
+
].includes(String(entry.state)) || entry.platform === "android" && !hostedConsolePort(entry.consolePort) || (entry.platform === "macos" ? !hostedMacosAppSlot(entry.appSlot) : entry.appSlot !== void 0) || entry.device !== null && !parseHostedPlatformDevice(entry.device, entry.platform) || entry.platform === "macos" && entry.device !== null && entry.device.appSlot !== entry.appSlot || entry.platform === "android" && entry.device !== null && entry.device.consolePort !== entry.consolePort || entry.state === "ready" && entry.device === null || entry.parked !== void 0 && (!isJsonObject(entry.parked) || typeof entry.parked.at !== "string" || !Number.isFinite(Date.parse(entry.parked.at)) || new Date(entry.parked.at).toISOString() !== entry.parked.at || entry.state !== "stopped" || entry.platform === "macos") || entry.notice !== void 0 && typeof entry.notice !== "string" || entry.appAttempt !== void 0 && (typeof entry.appAttempt !== "string" || !/^[a-zA-Z0-9_-]{1,128}$/.test(entry.appAttempt)) || entry.clientMetroPort !== void 0 && (typeof entry.clientMetroPort !== "number" || !Number.isInteger(entry.clientMetroPort) || entry.clientMetroPort < 1 || entry.clientMetroPort > 65535 || entry.platform !== "android") || entry.metroPort !== void 0 && (typeof entry.metroPort !== "number" || !Number.isInteger(entry.metroPort) || entry.metroPort < 1 || entry.metroPort > 65535)) throw new Error(`Malformed hosted session record: ${root}`);
|
|
3995
4158
|
ids.add(entry.id);
|
|
3996
4159
|
return entry;
|
|
3997
4160
|
});
|
|
@@ -4270,27 +4433,45 @@ function macosAppState(record) {
|
|
|
4270
4433
|
};
|
|
4271
4434
|
}
|
|
4272
4435
|
//#endregion
|
|
4273
|
-
//#region state/hosted-
|
|
4274
|
-
function
|
|
4436
|
+
//#region state/hosted-native.ts
|
|
4437
|
+
function parseHostedNativePlacement(value, platform) {
|
|
4275
4438
|
if (!isJsonObject(value) || typeof value.machine !== "string" || !parseMachine(value.machine) || typeof value.session !== "string" || !/^[a-f0-9-]{36}$/.test(value.session)) return null;
|
|
4276
|
-
const device = parseHostedDevice(value.device);
|
|
4277
4439
|
return {
|
|
4278
4440
|
machine: value.machine,
|
|
4279
4441
|
session: value.session,
|
|
4280
4442
|
appAttempt: typeof value.appAttempt === "string" && /^[a-zA-Z0-9_-]{1,128}$/.test(value.appAttempt) ? value.appAttempt : "",
|
|
4281
|
-
selected: value.machine,
|
|
4282
|
-
|
|
4443
|
+
selected: typeof value.selected === "string" && parseMachine(value.selected) ? value.selected : value.machine,
|
|
4444
|
+
...typeof value.reason === "string" ? { reason: value.reason } : {},
|
|
4445
|
+
device: parseHostedPlatformDevice(value.device, platform),
|
|
4283
4446
|
agent: parseHostedAgentAccess(value.agent) ?? {
|
|
4284
4447
|
driver: "none",
|
|
4285
4448
|
setting: "hosting.agentDriver"
|
|
4286
4449
|
}
|
|
4287
4450
|
};
|
|
4288
4451
|
}
|
|
4452
|
+
function hostedNativeRecords(state, platform) {
|
|
4453
|
+
const slots = isJsonObject(state?.deviceSlots) ? state.deviceSlots : {};
|
|
4454
|
+
const records = {
|
|
4455
|
+
default: state?.[platform],
|
|
4456
|
+
...Object.fromEntries(Object.entries(slots).map(([slot, value]) => [slot, isJsonObject(value) ? value[platform] : void 0]))
|
|
4457
|
+
};
|
|
4458
|
+
return Object.fromEntries(Object.entries(records).flatMap(([slot, record]) => isJsonObject(record) && record.host !== void 0 ? [[slot, record.host]] : []));
|
|
4459
|
+
}
|
|
4460
|
+
function unreadableHostedNative(slot, platform) {
|
|
4461
|
+
const key = slot === "default" ? `${platform}.host` : `deviceSlots.${slot}.${platform}.host`;
|
|
4462
|
+
return `The hosted ${platform === "ios" ? "iOS" : "Android"} placement at workspace-state key ${key} is unreadable. Inspect ${key}.machine and ${key}.session before reconciling it.`;
|
|
4463
|
+
}
|
|
4464
|
+
//#endregion
|
|
4465
|
+
//#region state/hosted-ios.ts
|
|
4466
|
+
function parseHostedIosPlacement(value) {
|
|
4467
|
+
return parseHostedNativePlacement(value, "ios");
|
|
4468
|
+
}
|
|
4289
4469
|
function hostedIosStatus(placement) {
|
|
4290
4470
|
return {
|
|
4291
4471
|
machine: placement.machine,
|
|
4292
4472
|
session: placement.session,
|
|
4293
4473
|
selected: placement.selected,
|
|
4474
|
+
...placement.reason ? { reason: placement.reason } : {},
|
|
4294
4475
|
agent: placement.agent,
|
|
4295
4476
|
device: placement.device ? {
|
|
4296
4477
|
name: placement.device.deviceType,
|
|
@@ -4298,18 +4479,11 @@ function hostedIosStatus(placement) {
|
|
|
4298
4479
|
} : null
|
|
4299
4480
|
};
|
|
4300
4481
|
}
|
|
4301
|
-
/** Includes unreadable placements so callers retain every recorded hosting slot. */
|
|
4302
4482
|
function hostedIosRecords(state) {
|
|
4303
|
-
|
|
4304
|
-
const records = {
|
|
4305
|
-
default: state?.ios,
|
|
4306
|
-
...Object.fromEntries(Object.entries(slots).map(([slot, value]) => [slot, isJsonObject(value) ? value.ios : void 0]))
|
|
4307
|
-
};
|
|
4308
|
-
return Object.fromEntries(Object.entries(records).flatMap(([slot, record]) => isJsonObject(record) && record.host !== void 0 ? [[slot, record.host]] : []));
|
|
4483
|
+
return hostedNativeRecords(state, "ios");
|
|
4309
4484
|
}
|
|
4310
4485
|
function unreadableHostedIos(slot) {
|
|
4311
|
-
|
|
4312
|
-
return `The hosted iOS placement at workspace-state key ${key} is unreadable. Inspect ${key}.machine and ${key}.session before reconciling it.`;
|
|
4486
|
+
return unreadableHostedNative(slot, "ios");
|
|
4313
4487
|
}
|
|
4314
4488
|
function hostedIosPlacements(state) {
|
|
4315
4489
|
return Object.assign(Object.create(null), Object.fromEntries(Object.entries(hostedIosRecords(state)).flatMap(([slot, record]) => {
|
|
@@ -4318,6 +4492,40 @@ function hostedIosPlacements(state) {
|
|
|
4318
4492
|
})));
|
|
4319
4493
|
}
|
|
4320
4494
|
//#endregion
|
|
4495
|
+
//#region state/hosted-android.ts
|
|
4496
|
+
function parseHostedAndroidPlacement(value) {
|
|
4497
|
+
return parseHostedNativePlacement(value, "android");
|
|
4498
|
+
}
|
|
4499
|
+
function hostedAndroidStatus(placement) {
|
|
4500
|
+
return {
|
|
4501
|
+
machine: placement.machine,
|
|
4502
|
+
session: placement.session,
|
|
4503
|
+
selected: placement.selected,
|
|
4504
|
+
...placement.reason ? { reason: placement.reason } : {},
|
|
4505
|
+
agent: placement.agent,
|
|
4506
|
+
device: placement.device ? {
|
|
4507
|
+
name: hostedAndroidDeviceName(placement.device),
|
|
4508
|
+
systemImage: placement.device.systemImage,
|
|
4509
|
+
api: Number(/^system-images;android-(\d+);/.exec(placement.device.systemImage)?.[1])
|
|
4510
|
+
} : null
|
|
4511
|
+
};
|
|
4512
|
+
}
|
|
4513
|
+
function hostedAndroidRecords(state) {
|
|
4514
|
+
return hostedNativeRecords(state, "android");
|
|
4515
|
+
}
|
|
4516
|
+
function unreadableHostedAndroid(slot) {
|
|
4517
|
+
return unreadableHostedNative(slot, "android");
|
|
4518
|
+
}
|
|
4519
|
+
function hostedAndroidPlacements(state) {
|
|
4520
|
+
return Object.assign(Object.create(null), Object.fromEntries(Object.entries(hostedAndroidRecords(state)).flatMap(([slot, record]) => {
|
|
4521
|
+
const placement = parseHostedAndroidPlacement(record);
|
|
4522
|
+
return placement ? [[slot, placement]] : [];
|
|
4523
|
+
})));
|
|
4524
|
+
}
|
|
4525
|
+
function hostedAndroidDeviceName(device) {
|
|
4526
|
+
return `${device.deviceProfile} (API ${/^system-images;android-(\d+);/.exec(device.systemImage)?.[1]})`;
|
|
4527
|
+
}
|
|
4528
|
+
//#endregion
|
|
4321
4529
|
//#region state/hosted-metro.ts
|
|
4322
4530
|
const HOSTED_METRO_REQUESTS_KEY = "hostedMetroRequests";
|
|
4323
4531
|
const HOSTED_METRO_GATEWAYS_KEY = "hostedMetroGateways";
|
|
@@ -4360,15 +4568,19 @@ function hostedMacosLogsDir(home) {
|
|
|
4360
4568
|
return null;
|
|
4361
4569
|
}
|
|
4362
4570
|
}
|
|
4363
|
-
const
|
|
4571
|
+
const hostedNativeLogsDir = (home, platform) => join(home, `${platform}-logs`);
|
|
4364
4572
|
/** The completed query window and overlap digests, or null for a missing, malformed or unreadable checkpoint. */
|
|
4365
|
-
function
|
|
4573
|
+
function readHostedNativeLogsCheckpoint(home, platform = "ios") {
|
|
4366
4574
|
try {
|
|
4367
|
-
const value = JSON.parse(readFileSync(join(
|
|
4575
|
+
const value = JSON.parse(readFileSync(join(hostedNativeLogsDir(home, platform), "checkpoint.json"), "utf8"));
|
|
4368
4576
|
if (!isJsonObject(value) || !Number.isSafeInteger(value.until) || value.until < 0 || value.windowMs !== void 0 && (!Number.isSafeInteger(value.windowMs) || value.windowMs < 1e3) || !Array.isArray(value.boundary) || !value.boundary.every((each) => typeof each === "string" && /^[a-f0-9]{64}$/.test(each))) return null;
|
|
4369
4577
|
return {
|
|
4370
4578
|
until: value.until,
|
|
4371
4579
|
boundary: value.boundary,
|
|
4580
|
+
...typeof value.appAttempt === "string" && Number.isSafeInteger(value.pid) && value.pid > 0 ? {
|
|
4581
|
+
appAttempt: value.appAttempt,
|
|
4582
|
+
pid: value.pid
|
|
4583
|
+
} : {},
|
|
4372
4584
|
...typeof value.windowMs === "number" ? { windowMs: value.windowMs } : {}
|
|
4373
4585
|
};
|
|
4374
4586
|
} catch {
|
|
@@ -4535,6 +4747,7 @@ function readArchive(id) {
|
|
|
4535
4747
|
if (!id || id.startsWith(".") || basename(id) !== id || id.includes("\0")) return null;
|
|
4536
4748
|
try {
|
|
4537
4749
|
if (!lstatSync(join(archiveRoot(), id)).isDirectory()) return null;
|
|
4750
|
+
if (!lstatSync(join(archiveRoot(), id, "archive.json")).isFile()) return null;
|
|
4538
4751
|
} catch {
|
|
4539
4752
|
return null;
|
|
4540
4753
|
}
|
|
@@ -4556,6 +4769,17 @@ function readArchive(id) {
|
|
|
4556
4769
|
].some((key) => typeof bytes[key] !== "number" || !Number.isFinite(bytes[key]) || bytes[key] < 0)) return null;
|
|
4557
4770
|
return record;
|
|
4558
4771
|
}
|
|
4772
|
+
function readArchiveState(id) {
|
|
4773
|
+
if (!id || id.startsWith(".") || basename(id) !== id || id.includes("\0")) return null;
|
|
4774
|
+
const file = join(archiveRoot(), id, "state.json");
|
|
4775
|
+
try {
|
|
4776
|
+
if (!lstatSync(join(archiveRoot(), id)).isDirectory()) return null;
|
|
4777
|
+
if (!lstatSync(file).isFile()) return null;
|
|
4778
|
+
} catch {
|
|
4779
|
+
return null;
|
|
4780
|
+
}
|
|
4781
|
+
return readJsonObject(file);
|
|
4782
|
+
}
|
|
4559
4783
|
function archivedUsage(archives) {
|
|
4560
4784
|
const usage = {
|
|
4561
4785
|
count: archives.length,
|
|
@@ -4649,4 +4873,4 @@ function isSetupJournalPrunable(journal, now) {
|
|
|
4649
4873
|
return Date.parse(journal.expiresAt) + 36e5 < now;
|
|
4650
4874
|
}
|
|
4651
4875
|
//#endregion
|
|
4652
|
-
export { ACTIVITY_RECENCY_BASES, AGENT_TOOLS, ANDROID_COMPILER_CACHE_CHOICES, ANDROID_EMULATOR_APPS, ANDROID_PCH_CHOICES, APP_PROCESS_STATES, BUILD_HISTORY_KEY, BUILD_HISTORY_LIMIT, BUILD_PHASES, BUILD_RESULTS, DEVICE_IDLE_SHUTDOWN_KEY, DEV_SERVER_STOP_REQUEST_KEY, ENDED_AGENT_RETENTION_MS, ERROR_SOURCES, HISTORY_LIMIT, HOSTED_AGENT_DRIVERS, HOSTED_APP_CHUNK_BYTES, HOSTED_LOGS_FIRST_BYTES, HOSTED_LOGS_PAGE_BYTES, HOSTED_MACOS_APP_SLOTS, HOSTED_METRO_GATEWAYS_KEY, HOSTED_METRO_REQUESTS_KEY, IDLE_STOP_KEY, IOS_SIMULATOR_APPS, LAST_BUILD_KEYS, LEVELS, MACHINE_OWNER_KINDS, MEMORY_SOURCES, NATIVE_BUILD_STEPS, NATIVE_CRASH_LOG_PREFIX, OFFLOAD_MODES, PHYSICAL_DEVICE_CONNECTIONS, PLACEMENT_DECISIONS, READY_PHASE_MS, RECORDING_CAP_BYTES, RECORDING_FOOTAGE_MS, RECORDING_PLATFORMS, REMOTE_DEVICE_BACKENDS, SETTINGS, SETTINGS_SCHEMA_FILE, SETTINGS_SCHEMA_URL, SETTING_GROUPS, SETTING_SCOPES, SOURCES, STATS_VERSION, STATUS_ISSUE_CODES, TAILNET_MACHINE_PATTERN, TUNNEL_MODES, WARM_KEY, WEB_PAGE_STATES, WEB_VIEWPORTS, WORKSPACE_AGENT_KEY, WORKSPACE_PHASES, WORKSPACE_STAGE_KINDS, acceptsShape, advanceTail, agentDeviceStateDirs, agentDeviceUsageFile, agentSessionOf, agentSessionsCacheFile, allMetroPorts, archiveDir, archiveEnabled, archiveLock, archiveRoot, archivedUsage, assertHostedDeviceLedger, attachExpoErrorContext, buildCriteria, buildDiagnostics, buildMachinesFile, buildMachinesLock, buildSlotPath, buildSlotsDir, buildWorkerRoot, bundleMarkerPlatform, capDrops, closedSegmentName, coerceSettingText, compileGrep, configCorruptRepair, configLockPath, countErrorEntries, createAgentActionReader, createdDevicesFile, createdDevicesLock, decodeStats, deviceHostArea, deviceHostMachinesClaims, deviceHostMachinesFile, deviceHostMachinesLock, deviceHostRoot, deviceViewersDir, diskUsageCacheDir, diskUsageCacheFile, easMachineStateRoot, easSessionLedgerFile, easSessionLedgerLock, emulatorLogFile, endedAgentSessionOf, endpoint, expectedShape, fileSizes, findEnclosingWorktreeRoot, findPeer, findProjectByMetroPort, findProjectRoot, followLogs, footageDrops, getConcurrencyLimits, getConfigPath, getProject, getProjectSetting, getProjectSettings, getRepoSettings, gitMergeCacheDir, hostedAppArea, hostedAppAttempt, hostedAppBlobs, hostedConsolePort, hostedDeviceId,
|
|
4876
|
+
export { ACTIVITY_RECENCY_BASES, AGENT_TOOLS, ANDROID_COMPILER_CACHE_CHOICES, ANDROID_EMULATOR_APPS, ANDROID_PCH_CHOICES, APP_PROCESS_STATES, BUILD_HISTORY_KEY, BUILD_HISTORY_LIMIT, BUILD_PHASES, BUILD_RESULTS, CAPACITY_REFUSAL_LIMIT, CAPACITY_REFUSAL_MAX_AGE_MS, DEVICE_IDLE_SHUTDOWN_KEY, DEVICE_RECLAIM_REASON, DEV_SERVER_STOP_REQUEST_KEY, ENDED_AGENT_RETENTION_MS, ERROR_SOURCES, HISTORY_LIMIT, HOSTED_AGENT_DRIVERS, HOSTED_APP_CHUNK_BYTES, HOSTED_LOGS_FIRST_BYTES, HOSTED_LOGS_PAGE_BYTES, HOSTED_MACOS_APP_SLOTS, HOSTED_METRO_GATEWAYS_KEY, HOSTED_METRO_REQUESTS_KEY, IDLE_STOP_KEY, IOS_SIMULATOR_APPS, LAST_BUILD_KEYS, LEVELS, MACHINE_OWNER_KINDS, MEMORY_SOURCES, NATIVE_BUILD_STEPS, NATIVE_CRASH_LOG_PREFIX, OFFLOAD_MODES, PHYSICAL_DEVICE_CONNECTIONS, PLACEMENT_DECISIONS, READY_PHASE_MS, RECORDING_CAP_BYTES, RECORDING_FOOTAGE_MS, RECORDING_PLATFORMS, REMOTE_DEVICE_BACKENDS, SETTINGS, SETTINGS_SCHEMA_FILE, SETTINGS_SCHEMA_URL, SETTING_GROUPS, SETTING_SCOPES, SOURCES, STATS_VERSION, STATUS_ISSUE_CODES, TAILNET_MACHINE_PATTERN, TUNNEL_MODES, TUTORIAL_VERSION, WARM_KEY, WEB_PAGE_STATES, WEB_VIEWPORTS, WORKSPACE_AGENT_KEY, WORKSPACE_PHASES, WORKSPACE_STAGE_KINDS, acceptsShape, advanceTail, agentDeviceStateDirs, agentDeviceUsageFile, agentSessionOf, agentSessionsCacheFile, allMetroPorts, archiveDir, archiveEnabled, archiveLock, archiveRoot, archivedUsage, assertHostedDeviceLedger, attachExpoErrorContext, buildCriteria, buildDiagnostics, buildMachinesFile, buildMachinesLock, buildSlotPath, buildSlotsDir, buildWorkerRoot, bundleMarkerPlatform, capDrops, closedSegmentName, coerceSettingText, compileGrep, configCorruptRepair, configLockPath, countErrorEntries, createAgentActionReader, createdDevicesFile, createdDevicesLock, decodeStats, deviceHostArea, deviceHostMachinesClaims, deviceHostMachinesFile, deviceHostMachinesLock, deviceHostRoot, deviceViewersDir, diskUsageCacheDir, diskUsageCacheFile, easMachineStateRoot, easSessionLedgerFile, easSessionLedgerLock, emulatorLogFile, endedAgentSessionOf, endpoint, expectedShape, fileSizes, findEnclosingWorktreeRoot, findPeer, findProjectByMetroPort, findProjectRoot, followLogs, footageDrops, getConcurrencyLimits, getConfigPath, getParkedMax, getProject, getProjectSetting, getProjectSettings, getRepoSettings, gitMergeCacheDir, hostedAndroidDeviceName, hostedAndroidPlacements, hostedAndroidRecords, hostedAndroidStatus, hostedAppArea, hostedAppAttempt, hostedAppBlobs, hostedConsolePort, hostedDeviceId, hostedIosPlacements, hostedIosRecords, hostedIosStatus, hostedMacosAppSlot, hostedMacosBundleId, hostedMacosLogsDir, hostedMetroGateways, hostedMetroRequests, hostedNativeLogsDir, hostedNativeRecords, isJournalExpired, isJsonObject, isLayeredSetting, isPathPrefix, isSensitiveReference, isSetupJournalPrunable, lastUseFrom, levelRank, linkReplacedArchives, listRecordedDevices, listSegments, listStatusCacheEntries, loadConfig, logFiles, machineCapacity, macosAppState, maintenanceAttemptFile, maintenanceChildLogFile, maintenanceDir, maintenanceNdjsonFile, maintenanceRunClaims, maintenanceStateFile, markerWindow, offloadSummary, openSegmentName, parseHostedAgentAccess, parseHostedAgentGrant, parseHostedAndroidDevice, parseHostedAndroidPlacement, parseHostedAppManifest, parseHostedAppOffer, parseHostedChoice, parseHostedDevice, parseHostedIosPlacement, parseHostedLocalDeviceCount, parseHostedLogsCursor, parseHostedMacosChoice, parseHostedMacosDevice, parseHostedMacosPlacement, parseHostedNativeOffer, parseHostedNativePlacement, parseHostedOfferRequest, parseHostedPlatformDevice, parseHostedRequest, parseMachine, parseMacosRecord, parseMaintenanceState, parseMissReason, parseNdjsonLine, parseNdjsonText, parseSetupJournal, parseSince, pinnedEndpoint, pullRequestCacheDir, pullRequestCacheFile, queryJsonLogs, queryLogs, readAgentDeviceUsage, readAgentDeviceUsageCache, readAgentSessionsCache, readArchive, readArchiveState, readArchives, readBuildDetail, readBuildHistory, readBuildMachines, readCreatedDevices, readDevServerStop, readDevServerStopRequest, readDeviceHostMachines, readDeviceIdleShutdowns, readDevicePlacement, readDiskUsage, readEasSessionLedger, readEndedAgentSessions, readHostedApp, readHostedAppMetadata, readHostedDevice, readHostedDeviceLedger, readHostedMacosApp, readHostedNativeLogsCheckpoint, readHostedSessions, readIdleStop, readJsonFile, readJsonObject, readLastBuilds, readLogRecords, readLogTimeline, readLogsSince, readMacosRecord, readMaintenanceAttempt, readMaintenanceState, readNdjsonGenerations, readPullRequestCache, readRunEstimates, readSetupJournal, readStateFile, readStats, readStatsReport, readSwiftpmCacheUsage, readViewedDevices, readWarmRecord, readWorkspaceAgent, readWorkspaceState, recordMatches, recordedBytes, recordingDeviceName, recordingEnabled, rememberHostedAppBlobRename, saturation, serverSetupDir, settingDefinition, settingValueError, settingsJsonSchema, setupJournalFile, sharedCcache, sharedCompilationCache, sharedGradle, sharedPods, sortByTs, statsFile, statsProjectKey, stimBuildDigest, supervisorLogFile, supervisorPidFile, swiftpmCacheUsageFile, tailRead, trimCapacityRefusals, trimCapacityWaits, trimSamples, tryAcquireBuildSlotClaim, unreadableHostedAndroid, unreadableHostedIos, unreadableHostedNative, validEasSessionClaim, validHostedAppArguments, validHostedAppBlob, wholePhases, withConfigLock, withDerivedFacts, withGitChip, withStateReadCache, workspaceAgentDeviceDir, workspaceBuildDetailFile, workspaceDerivedData, workspaceEndedAgentsFile, workspaceEndedAgentsLock, workspaceGradleBuild, workspaceLastUsed, workspaceLogErrorIndex, workspaceLogsDir, workspaceMetadataFile, workspaceRecordingsDir, workspaceStateFile, workspaceStateLock };
|