stim 1.9.0 → 1.10.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/dist/{android-_y4Ohi5z.mjs → android-787HhP5w.mjs} +119 -29
- package/dist/{android-BPGizezh.mjs → android-DBkeHwpm.mjs} +168 -95
- package/dist/{android-DEgo4oCo.mjs → android-DPov-fv2.mjs} +2 -2
- package/dist/{android-cas-eiExNLf7.mjs → android-cas-CITmIeWY.mjs} +9 -8
- package/dist/android-cas-compiler.mjs +1 -1
- package/dist/{app-install-Uf3a83Tv.mjs → app-install-CrJ6t_bv.mjs} +2 -2
- package/dist/budget-BP4jZZL4.mjs +620 -0
- package/dist/build-progress-BXx-ZBe4.mjs +512 -0
- package/dist/{cache-manifest-Dg7OnMrp.mjs → cache-manifest-Pubfp8wV.mjs} +1 -1
- package/dist/cache-manifest.mjs +1 -1
- package/dist/cli.mjs +16 -16
- package/dist/collector-run.d.mts +5 -1
- package/dist/collector-run.mjs +7 -7
- package/dist/{command-output-gByVmIjy.mjs → command-output-D5z2_bcF.mjs} +1 -1
- package/dist/{config-Cp9lM3wX.mjs → config-BEq0Qw_a.mjs} +14 -131
- package/dist/{dependency-state-Bq5DcLGn.mjs → dependency-state-r9UesA9a.mjs} +2 -2
- package/dist/{deps-Cip5vUm1.mjs → deps-DP4eWqea.mjs} +3 -3
- package/dist/{detached-entry-KEu0_AXL.mjs → detached-entry-DJHoca49.mjs} +1 -1
- package/dist/{dev-client-HyqISM0h.mjs → dev-client-92U593KT.mjs} +137 -250
- package/dist/{device-D4U-xLty.mjs → device-DhTZe7L4.mjs} +7 -7
- package/dist/{build-lock-DPXPLgTc.mjs → device-lease-CbEaIk7b.mjs} +370 -12
- package/dist/{device-pool-CnTAz8X1.mjs → device-pool-Di8Q-9Ro.mjs} +4 -4
- package/dist/{device-remote-AQ5rQPqr.mjs → device-remote-Dy1jGj3U.mjs} +547 -468
- package/dist/{doctor-B_Ei3bZu.mjs → doctor-B-inAJmt.mjs} +58 -22
- package/dist/{error-diagnostics-Vfz83GCF.mjs → error-diagnostics-xxWKNO3-.mjs} +11 -12
- package/dist/{exec-jIAlgilk.mjs → exec-DK8KaVHK.mjs} +3 -2
- package/dist/{gc-Ckw8RQHi.mjs → gc-ChPFZMRC.mjs} +500 -1114
- package/dist/{guide-BiAbhs1J.mjs → guide-DwmaD_Wa.mjs} +882 -528
- package/dist/guide-status-CWvti2Lg.mjs +125 -0
- package/dist/idle-DpRLp5lj.mjs +157 -0
- package/dist/in-use-CjUluqIX.mjs +245 -0
- package/dist/{ios-BL2NFckr.mjs → ios-B2Fpqvlb.mjs} +25 -59
- package/dist/{ios-BzUJWk1K.mjs → ios-Dd-hYJBh.mjs} +130 -89
- package/dist/{ios-device-DPsSm0v_.mjs → ios-device-BiOsSVzd.mjs} +1 -1
- package/dist/{ios-device-D972i9wf.mjs → ios-device-CtRdEVjx.mjs} +3 -3
- package/dist/{launch-verify-Q_OtW1Rf.mjs → launch-verify-CQS_Kg-C.mjs} +5 -5
- package/dist/{logs-CI9NQSK7.mjs → logs-MARIy6m9.mjs} +9 -9
- package/dist/{metro-DrLHaMn2.mjs → metro-BWLOJ_Wr.mjs} +116 -10
- package/dist/{named-ports-CmLQSIRj.mjs → named-ports-DiTfSz6p.mjs} +4 -4
- package/dist/{ndjson-9uFNRwfe.mjs → ndjson-CDY9IkVC.mjs} +3 -56
- package/dist/{ndjson-DvHSsNU9.d.mts → ndjson-Du3j0AQ9.d.mts} +1 -0
- package/dist/{ownership-D15Op8vN.mjs → ownership-D2xbLVoz.mjs} +2 -3
- package/dist/ownership-DZjdx8Gq.mjs +1180 -0
- package/dist/{ports-BNKoWVWZ.mjs → ports-CLSSFOuz.mjs} +3 -3
- package/dist/{project-DS112DYB.mjs → project-BXZzOABt.mjs} +2 -2
- package/dist/{reload-ZNJUo1pl.mjs → reload-DoETJ3rl.mjs} +9 -10
- package/dist/{server-bare-DWkkcIxV.mjs → server-bare-DVbb7kXH.mjs} +1 -1
- package/dist/{server-expo-B_lctBR3.mjs → server-expo-BvOCeakp.mjs} +4 -4
- package/dist/server-expo-DBVTdult.mjs +2 -0
- package/dist/{settings-D030k0t4.mjs → settings-DOugPlPz.mjs} +16 -428
- package/dist/{settings-BKNh5rl4.mjs → settings-EWYNwe72.mjs} +16 -14
- package/dist/settings.schema.json +75 -0
- package/dist/{simslim-B_LsGLuJ.mjs → simslim-Ca3487H6.mjs} +182 -5
- package/dist/{slot-launch-DU9A-gYw.mjs → slot-launch-C5cg0fmk.mjs} +4 -4
- package/dist/{spawn-claims-Dn3C_N6w.mjs → spawn-claims-CUleC2cM.mjs} +1 -1
- package/dist/{start-DYkkKF0s.mjs → start-F9gv5dcC.mjs} +147 -98
- package/dist/start-VoFsPSes.mjs +2 -0
- package/dist/{state-NcJzMEBF.mjs → state-WGdIbi_d.mjs} +2 -2
- package/dist/{stats-DG9e66Rz.mjs → stats-BAgJaLA4.mjs} +1 -1
- package/dist/{stats-B63-JVAY.mjs → stats-DBYJ5bxL.mjs} +4 -4
- package/dist/{status-S6ehL4Ui.mjs → status-DLCD-o3s.mjs} +63 -231
- package/dist/status-Dr4fRkym.mjs +216 -0
- package/dist/{guide-status-BAMVU0ZA.mjs → stim-installations-CINXy_su.mjs} +3 -124
- package/dist/{stop-BO28HGqF.mjs → stop-Dj4pfmyv.mjs} +22 -18
- package/dist/{stop-T1R-cZRz.mjs → stop-ZvOeTmKb.mjs} +2 -2
- package/dist/supervisor-run.d.mts +11 -2
- package/dist/supervisor-run.mjs +185 -17
- package/dist/{workspace-process-lock-DWaiRD0e.mjs → workspace-process-lock-BXA96myD.mjs} +2 -2
- package/dist/workspaces-BTUyFfkv.mjs +1003 -0
- package/dist/{worktree-DKhSF0UX.mjs → worktree-4aZg2NnI.mjs} +1 -1
- package/dist/{worktree-LGB-UwgG.mjs → worktree-BMS7GeqB.mjs} +40 -122
- package/dist/{worktree-R_cUlE7s.mjs → worktree-BlizKDpZ.mjs} +143 -18
- package/dist/{xcode-BZqyxh3e.mjs → xcode-B2tBkf0m.mjs} +9 -158
- package/package.json +6 -5
- package/dist/build-progress-CVqkfPC6.mjs +0 -188
- package/dist/device-lease-Crs-HkJY.mjs +0 -366
- package/dist/logs-query-Cqma3WIu.mjs +0 -298
- package/dist/ownership-DCQPuWEc.mjs +0 -489
- package/dist/public-http-probe-CyMDrQ3c.mjs +0 -61
- package/dist/server-expo-CeILEu86.mjs +0 -2
- package/dist/sim-pool-KaIsp2Ft.mjs +0 -723
- package/dist/spawn-entry-nCE6m770.mjs +0 -14
- package/dist/workspace-state-DF2gCFhx.mjs +0 -169
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import { k as ownership_claim_exports } from "./config-Cp9lM3wX.mjs";
|
|
2
|
-
import { a as updateWorkspaceState, t as clearWorkspaceStateKey } from "./workspace-state-DF2gCFhx.mjs";
|
|
3
|
-
import { a as formatElapsed, u as plural } from "./command-output-gByVmIjy.mjs";
|
|
4
|
-
//#region src/engine/build-progress.ts
|
|
5
|
-
const BUILD_PHASES = [
|
|
6
|
-
"prepare",
|
|
7
|
-
"cache-lookup",
|
|
8
|
-
"wait",
|
|
9
|
-
"prebuild",
|
|
10
|
-
"pods",
|
|
11
|
-
"compile",
|
|
12
|
-
"install",
|
|
13
|
-
"launch"
|
|
14
|
-
];
|
|
15
|
-
const ACTIVE_BUILD_KEY = "activeBuild";
|
|
16
|
-
const NO_BUILD_PROGRESS = {
|
|
17
|
-
step: () => {},
|
|
18
|
-
durations: () => ({}),
|
|
19
|
-
clear: () => {}
|
|
20
|
-
};
|
|
21
|
-
const COLD_PHASES = [
|
|
22
|
-
"prebuild",
|
|
23
|
-
"pods",
|
|
24
|
-
"compile"
|
|
25
|
-
];
|
|
26
|
-
const DEVICE_PHASES = ["install", "launch"];
|
|
27
|
-
function startBuildProgress({ root, platform, slot, claim, now = Date.now, note = () => {} }) {
|
|
28
|
-
const startedAt = new Date(now()).toISOString();
|
|
29
|
-
const record = {
|
|
30
|
-
platform,
|
|
31
|
-
slot,
|
|
32
|
-
startedAt,
|
|
33
|
-
phase: "prepare",
|
|
34
|
-
phaseStartedAt: startedAt,
|
|
35
|
-
phases: [{
|
|
36
|
-
phase: "prepare",
|
|
37
|
-
startedAt
|
|
38
|
-
}],
|
|
39
|
-
claim: {
|
|
40
|
-
root: claim.root,
|
|
41
|
-
path: claim.path,
|
|
42
|
-
claimId: claim.claimId,
|
|
43
|
-
pid: claim.owner.pid
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
let warned = false;
|
|
47
|
-
const guard = (fn) => {
|
|
48
|
-
try {
|
|
49
|
-
fn();
|
|
50
|
-
} catch (error) {
|
|
51
|
-
if (warned) return;
|
|
52
|
-
warned = true;
|
|
53
|
-
note(`Build progress could not be recorded in the workspace state: ${error?.message || error}`);
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
const write = () => guard(() => updateWorkspaceState(root, (state) => ({
|
|
57
|
-
...state,
|
|
58
|
-
[ACTIVE_BUILD_KEY]: record
|
|
59
|
-
})));
|
|
60
|
-
write();
|
|
61
|
-
return {
|
|
62
|
-
step(phase) {
|
|
63
|
-
if (phase === record.phase) return;
|
|
64
|
-
const at = new Date(now()).toISOString();
|
|
65
|
-
record.phase = phase;
|
|
66
|
-
record.phaseStartedAt = at;
|
|
67
|
-
record.phases.push({
|
|
68
|
-
phase,
|
|
69
|
-
startedAt: at
|
|
70
|
-
});
|
|
71
|
-
write();
|
|
72
|
-
},
|
|
73
|
-
durations() {
|
|
74
|
-
return phaseDurations(record.phases, now());
|
|
75
|
-
},
|
|
76
|
-
clear() {
|
|
77
|
-
guard(() => {
|
|
78
|
-
clearWorkspaceStateKey(root, ACTIVE_BUILD_KEY, (value) => parseActiveBuild(value)?.claim.claimId === record.claim.claimId);
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
function phaseDurations(phases, now) {
|
|
84
|
-
const out = {};
|
|
85
|
-
phases.forEach(({ phase, startedAt }, index) => {
|
|
86
|
-
const start = Date.parse(startedAt);
|
|
87
|
-
const next = phases[index + 1];
|
|
88
|
-
const end = next ? Date.parse(next.startedAt) : now;
|
|
89
|
-
if (!Number.isFinite(start) || !Number.isFinite(end)) return;
|
|
90
|
-
out[phase] = (out[phase] ?? 0) + Math.max(0, end - start);
|
|
91
|
-
});
|
|
92
|
-
return out;
|
|
93
|
-
}
|
|
94
|
-
function parseActiveBuild(value) {
|
|
95
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
96
|
-
const record = value;
|
|
97
|
-
const claim = record.claim;
|
|
98
|
-
if (record.platform !== "ios" && record.platform !== "android") return null;
|
|
99
|
-
if (!isPhase(record.phase)) return null;
|
|
100
|
-
if (typeof record.startedAt !== "string" || typeof record.phaseStartedAt !== "string") return null;
|
|
101
|
-
if (!claim || typeof claim.root !== "string" || typeof claim.claimId !== "string") return null;
|
|
102
|
-
const phases = Array.isArray(record.phases) ? record.phases.filter((entry) => isPhase(entry?.phase) && typeof entry?.startedAt === "string") : [];
|
|
103
|
-
return {
|
|
104
|
-
platform: record.platform,
|
|
105
|
-
slot: typeof record.slot === "string" ? record.slot : "default",
|
|
106
|
-
startedAt: record.startedAt,
|
|
107
|
-
phase: record.phase,
|
|
108
|
-
phaseStartedAt: record.phaseStartedAt,
|
|
109
|
-
phases,
|
|
110
|
-
claim: {
|
|
111
|
-
root: claim.root,
|
|
112
|
-
path: typeof claim.path === "string" ? claim.path : "",
|
|
113
|
-
claimId: claim.claimId,
|
|
114
|
-
pid: typeof claim.pid === "number" ? claim.pid : 0
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
function isPhase(value) {
|
|
119
|
-
return BUILD_PHASES.includes(value);
|
|
120
|
-
}
|
|
121
|
-
function activeBuildState(claim, survey = (0, ownership_claim_exports.readClaimSet)(claim.root)) {
|
|
122
|
-
if (survey.live.some((holder) => holder.claimId === claim.claimId)) return "running";
|
|
123
|
-
if (survey.dead.some((holder) => holder.claimId === claim.claimId)) return "stale";
|
|
124
|
-
return survey.unresolved.length ? "unknown" : "stale";
|
|
125
|
-
}
|
|
126
|
-
function liveOutcome(record) {
|
|
127
|
-
const seen = new Set(record.phases.map((entry) => entry.phase));
|
|
128
|
-
if (COLD_PHASES.some((phase) => seen.has(phase))) return "cold";
|
|
129
|
-
if (DEVICE_PHASES.some((phase) => seen.has(phase))) return "hit";
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
132
|
-
function median(values) {
|
|
133
|
-
if (!values.length) return null;
|
|
134
|
-
const sorted = values.toSorted((a, b) => a - b);
|
|
135
|
-
const mid = Math.floor(sorted.length / 2);
|
|
136
|
-
return sorted.length % 2 ? sorted[mid] : Math.round((sorted[mid - 1] + sorted[mid]) / 2);
|
|
137
|
-
}
|
|
138
|
-
function estimateBuild(history, platform, known, phase) {
|
|
139
|
-
const lists = history?.[platform];
|
|
140
|
-
const outcome = known ?? latestOutcome(lists);
|
|
141
|
-
const samples = outcome && lists?.[outcome] || [];
|
|
142
|
-
const phaseSamples = samples.flatMap((sample) => phase in sample.phases ? [sample.phases[phase]] : []);
|
|
143
|
-
return {
|
|
144
|
-
outcome,
|
|
145
|
-
expectedMs: median(samples.map((sample) => sample.durationMs)),
|
|
146
|
-
expectedPhaseMs: median(phaseSamples),
|
|
147
|
-
basis: samples.length
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
function latestOutcome(lists) {
|
|
151
|
-
let latest = null;
|
|
152
|
-
for (const outcome of ["hit", "cold"]) {
|
|
153
|
-
const last = lists?.[outcome]?.at(-1);
|
|
154
|
-
const at = last ? Date.parse(last.at) : NaN;
|
|
155
|
-
if (Number.isFinite(at) && (!latest || at > latest.at)) latest = {
|
|
156
|
-
outcome,
|
|
157
|
-
at
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
return latest?.outcome ?? null;
|
|
161
|
-
}
|
|
162
|
-
function buildReport(record, { state, history }) {
|
|
163
|
-
return {
|
|
164
|
-
platform: record.platform,
|
|
165
|
-
slot: record.slot,
|
|
166
|
-
state,
|
|
167
|
-
phase: record.phase,
|
|
168
|
-
startedAt: record.startedAt,
|
|
169
|
-
phaseStartedAt: record.phaseStartedAt,
|
|
170
|
-
...estimateBuild(history, record.platform, liveOutcome(record), record.phase)
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
function remainingText(remainingMs) {
|
|
174
|
-
return remainingMs < 6e4 ? "under a minute left" : `about ${Math.ceil(remainingMs / 6e4)} min left`;
|
|
175
|
-
}
|
|
176
|
-
function buildStatusLine(report, now) {
|
|
177
|
-
const slot = report.slot === "default" ? "" : ` [${report.slot}]`;
|
|
178
|
-
if (report.state === "stale") return `build: stale ${report.platform}${slot} record from ${report.startedAt} (its run is gone; the next run replaces it)`;
|
|
179
|
-
const elapsedMs = Math.max(0, now - Date.parse(report.startedAt));
|
|
180
|
-
const head = `build: ${report.platform}${slot} ${report.phase}, ${formatElapsed(elapsedMs)} elapsed`;
|
|
181
|
-
if (report.state === "unknown") return `${head} (its native-run claim cannot be resolved, so it may not be running)`;
|
|
182
|
-
if (report.expectedMs === null) return head;
|
|
183
|
-
const basis = `median of ${plural(report.basis, `${report.outcome} run`)}`;
|
|
184
|
-
const remainingMs = report.expectedMs - elapsedMs;
|
|
185
|
-
return remainingMs > 0 ? `${head} -- ${remainingText(remainingMs)} (${basis})` : `${head} (usually ~${formatElapsed(report.expectedMs)}, ${basis})`;
|
|
186
|
-
}
|
|
187
|
-
//#endregion
|
|
188
|
-
export { buildStatusLine as a, buildReport as i, NO_BUILD_PROGRESS as n, parseActiveBuild as o, activeBuildState as r, startBuildProgress as s, ACTIVE_BUILD_KEY as t };
|
|
@@ -1,366 +0,0 @@
|
|
|
1
|
-
import { A as withDirLock, H as deviceSlotKey, J as validateDeviceSlot, W as parseDeviceSlotKey, u as getConfigDir } from "./config-Cp9lM3wX.mjs";
|
|
2
|
-
import { c as writeWorkspaceState, n as clearWorkspaceStateKeys, o as withWorkspaceStateLock, r as readWorkspaceState } from "./workspace-state-DF2gCFhx.mjs";
|
|
3
|
-
import { i as segment } from "./build-lock-DPXPLgTc.mjs";
|
|
4
|
-
import { mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
import { randomBytes } from "node:crypto";
|
|
7
|
-
const DEFAULT_LEASE_MS = 3e5;
|
|
8
|
-
const LEASE_SUFFIX = ".json";
|
|
9
|
-
const LOCK_SUFFIX = ".lock";
|
|
10
|
-
const DURATION = /^[1-9][0-9]*(s|m)$/;
|
|
11
|
-
function deviceLocksDir() {
|
|
12
|
-
return join(getConfigDir(), "device-locks");
|
|
13
|
-
}
|
|
14
|
-
function leaseStem(platform, id) {
|
|
15
|
-
return `${segment(platform)}-${segment(id)}`;
|
|
16
|
-
}
|
|
17
|
-
function deviceLeasePath(platform, id) {
|
|
18
|
-
return join(deviceLocksDir(), `${leaseStem(platform, id)}${LEASE_SUFFIX}`);
|
|
19
|
-
}
|
|
20
|
-
function deviceLeaseLockPath(platform, id) {
|
|
21
|
-
return join(deviceLocksDir(), `${leaseStem(platform, id)}${LOCK_SUFFIX}`);
|
|
22
|
-
}
|
|
23
|
-
const fileLeaseIo = {
|
|
24
|
-
withHolderLock: withWorkspaceStateLock,
|
|
25
|
-
now: () => Date.now(),
|
|
26
|
-
readLease(path) {
|
|
27
|
-
try {
|
|
28
|
-
return readFileSync(path, "utf-8");
|
|
29
|
-
} catch {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
writeLease(path, body) {
|
|
34
|
-
mkdirSync(deviceLocksDir(), { recursive: true });
|
|
35
|
-
const tmp = `${path}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
36
|
-
writeFileSync(tmp, body);
|
|
37
|
-
try {
|
|
38
|
-
renameSync(tmp, path);
|
|
39
|
-
} catch (error) {
|
|
40
|
-
rmSync(tmp, { force: true });
|
|
41
|
-
throw error;
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
removeLease(path) {
|
|
45
|
-
rmSync(path, { force: true });
|
|
46
|
-
},
|
|
47
|
-
listLeaseNames() {
|
|
48
|
-
try {
|
|
49
|
-
return readdirSync(deviceLocksDir());
|
|
50
|
-
} catch {
|
|
51
|
-
return [];
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
|
-
withLeaseLock(lockPath, fn) {
|
|
55
|
-
return withDirLock(lockPath, fn, { ensureParent: () => mkdirSync(deviceLocksDir(), { recursive: true }) });
|
|
56
|
-
},
|
|
57
|
-
readHolder(root) {
|
|
58
|
-
return parseWorkspaceLeases(readWorkspaceState(root)?.deviceLeases);
|
|
59
|
-
},
|
|
60
|
-
writeHolder(root, leases) {
|
|
61
|
-
if (Object.keys(leases).length === 0) clearWorkspaceStateKeys(root, ["deviceLeases"]);
|
|
62
|
-
else writeWorkspaceState(root, { deviceLeases: leases });
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
function text(value) {
|
|
66
|
-
return typeof value === "string" && value.trim() !== "" ? value : null;
|
|
67
|
-
}
|
|
68
|
-
function isPlatform(value) {
|
|
69
|
-
return value === "ios" || value === "android";
|
|
70
|
-
}
|
|
71
|
-
function parseLease(raw) {
|
|
72
|
-
if (typeof raw !== "string") return null;
|
|
73
|
-
let parsed;
|
|
74
|
-
try {
|
|
75
|
-
parsed = JSON.parse(raw);
|
|
76
|
-
} catch {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
80
|
-
const entry = parsed;
|
|
81
|
-
if (entry.version !== 1 || !isPlatform(entry.platform)) return null;
|
|
82
|
-
const id = text(entry.id);
|
|
83
|
-
const holder = text(entry.holder);
|
|
84
|
-
const token = text(entry.token);
|
|
85
|
-
const expiresAt = text(entry.expiresAt);
|
|
86
|
-
if (!id || !holder || !token || !expiresAt || !Number.isFinite(Date.parse(expiresAt))) return null;
|
|
87
|
-
if (entry.slot !== void 0) {
|
|
88
|
-
if (typeof entry.slot !== "string") return null;
|
|
89
|
-
try {
|
|
90
|
-
validateDeviceSlot(entry.slot);
|
|
91
|
-
} catch {
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return {
|
|
96
|
-
version: 1,
|
|
97
|
-
...typeof entry.slot === "string" ? { slot: entry.slot } : {},
|
|
98
|
-
platform: entry.platform,
|
|
99
|
-
id,
|
|
100
|
-
deviceName: text(entry.deviceName),
|
|
101
|
-
holder,
|
|
102
|
-
token,
|
|
103
|
-
grantedAt: text(entry.grantedAt),
|
|
104
|
-
expiresAt
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
function parseWorkspaceLeases(value) {
|
|
108
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
109
|
-
const leases = {};
|
|
110
|
-
for (const [platform, entry] of Object.entries(value)) {
|
|
111
|
-
if (!parseDeviceSlotKey(platform) || !entry || typeof entry !== "object") continue;
|
|
112
|
-
const record = entry;
|
|
113
|
-
const id = text(record.id);
|
|
114
|
-
const token = text(record.token);
|
|
115
|
-
if (!id || !token) continue;
|
|
116
|
-
leases[platform] = {
|
|
117
|
-
id,
|
|
118
|
-
token,
|
|
119
|
-
kind: record.kind === "run" ? "run" : "declared"
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
return leases;
|
|
123
|
-
}
|
|
124
|
-
function leaseIsExpired(lease, now) {
|
|
125
|
-
return Date.parse(lease.expiresAt) <= now;
|
|
126
|
-
}
|
|
127
|
-
function parseLeaseDuration(value) {
|
|
128
|
-
const raw = String(value ?? "").trim();
|
|
129
|
-
const match = DURATION.exec(raw);
|
|
130
|
-
if (!match) return { error: `--for must be a whole number of seconds or minutes, such as 90s or 5m (got "${raw}").` };
|
|
131
|
-
const ms = Number(raw.slice(0, -1)) * (match[1] === "s" ? 1e3 : 6e4);
|
|
132
|
-
if (ms < 1e4 || ms > 18e5) return { error: `--for must be between 10s and 30m (got "${raw}").` };
|
|
133
|
-
return { ms };
|
|
134
|
-
}
|
|
135
|
-
function listLeaseFiles(io = fileLeaseIo) {
|
|
136
|
-
const dir = deviceLocksDir();
|
|
137
|
-
const entries = [];
|
|
138
|
-
for (const name of io.listLeaseNames().toSorted()) {
|
|
139
|
-
if (!name.endsWith(LEASE_SUFFIX)) continue;
|
|
140
|
-
const path = join(dir, name);
|
|
141
|
-
const lease = parseLease(io.readLease(path));
|
|
142
|
-
const stem = name.slice(0, -5);
|
|
143
|
-
const cut = stem.indexOf("-");
|
|
144
|
-
entries.push({
|
|
145
|
-
path,
|
|
146
|
-
name,
|
|
147
|
-
platform: lease?.platform ?? (cut > 0 ? stem.slice(0, cut) : stem),
|
|
148
|
-
id: lease?.id ?? (cut > 0 ? stem.slice(cut + 1) : null),
|
|
149
|
-
lease
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
return entries;
|
|
153
|
-
}
|
|
154
|
-
function writeLease(lease, io) {
|
|
155
|
-
io.writeLease(deviceLeasePath(lease.platform, lease.id), `${JSON.stringify(lease, null, 2)}\n`);
|
|
156
|
-
return lease;
|
|
157
|
-
}
|
|
158
|
-
function takeLease({ root, platform, slot = "default", id, deviceName = null, kind, durationMs = DEFAULT_LEASE_MS }, io = fileLeaseIo) {
|
|
159
|
-
return withHolderLock(io, root, () => {
|
|
160
|
-
const key = deviceSlotKey(platform, slot);
|
|
161
|
-
const previous = io.readHolder(root)[key];
|
|
162
|
-
if (previous && previous.id !== id) releaseHeld(root, platform, previous, io, slot);
|
|
163
|
-
for (const entry of listLeaseFiles(io)) {
|
|
164
|
-
const other = entry.lease;
|
|
165
|
-
if (!other || other.platform !== platform || other.holder !== root || (other.slot ?? "default") !== slot || other.id === id) continue;
|
|
166
|
-
releaseByRoot(root, platform, other.id, io);
|
|
167
|
-
}
|
|
168
|
-
const declared = previous?.kind === "declared" && previous.id === id;
|
|
169
|
-
const path = deviceLeasePath(platform, id);
|
|
170
|
-
const result = io.withLeaseLock(deviceLeaseLockPath(platform, id), () => {
|
|
171
|
-
const now = io.now();
|
|
172
|
-
const raw = io.readLease(path);
|
|
173
|
-
const current = parseLease(raw);
|
|
174
|
-
if (raw !== null && !current) return {
|
|
175
|
-
status: "unreadable",
|
|
176
|
-
path
|
|
177
|
-
};
|
|
178
|
-
const asked = now + durationMs;
|
|
179
|
-
if (current && !leaseIsExpired(current, now)) {
|
|
180
|
-
if (current.holder !== root || (current.slot ?? "default") !== slot) return {
|
|
181
|
-
status: "held",
|
|
182
|
-
lease: current
|
|
183
|
-
};
|
|
184
|
-
const floor = declared && kind === "run" ? Math.max(asked, Date.parse(current.expiresAt)) : asked;
|
|
185
|
-
return {
|
|
186
|
-
status: "set",
|
|
187
|
-
lease: writeLease({
|
|
188
|
-
...current,
|
|
189
|
-
deviceName: deviceName ?? current.deviceName,
|
|
190
|
-
expiresAt: new Date(floor).toISOString()
|
|
191
|
-
}, io)
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
const expiresAt = new Date(asked).toISOString();
|
|
195
|
-
return {
|
|
196
|
-
status: "taken",
|
|
197
|
-
lease: writeLease({
|
|
198
|
-
version: 1,
|
|
199
|
-
...slot === "default" ? {} : { slot },
|
|
200
|
-
platform,
|
|
201
|
-
id,
|
|
202
|
-
deviceName,
|
|
203
|
-
holder: root,
|
|
204
|
-
token: randomBytes(12).toString("hex"),
|
|
205
|
-
grantedAt: new Date(now).toISOString(),
|
|
206
|
-
expiresAt
|
|
207
|
-
}, io)
|
|
208
|
-
};
|
|
209
|
-
});
|
|
210
|
-
if (result.status === "taken" || result.status === "set") {
|
|
211
|
-
const recorded = result.status === "set" && declared ? "declared" : kind;
|
|
212
|
-
io.writeHolder(root, {
|
|
213
|
-
...io.readHolder(root),
|
|
214
|
-
[key]: {
|
|
215
|
-
id,
|
|
216
|
-
token: result.lease.token,
|
|
217
|
-
kind: recorded
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
return result;
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
function raiseLease({ root, platform, slot = "default", minMs }, io = fileLeaseIo) {
|
|
225
|
-
const record = io.readHolder(root)[deviceSlotKey(platform, slot)];
|
|
226
|
-
if (!record) return { status: "none" };
|
|
227
|
-
const path = deviceLeasePath(platform, record.id);
|
|
228
|
-
return io.withLeaseLock(deviceLeaseLockPath(platform, record.id), () => {
|
|
229
|
-
const current = parseLease(io.readLease(path));
|
|
230
|
-
if (!current || current.token !== record.token) return {
|
|
231
|
-
status: "lost",
|
|
232
|
-
lease: current
|
|
233
|
-
};
|
|
234
|
-
const floor = io.now() + minMs;
|
|
235
|
-
if (Date.parse(current.expiresAt) >= floor) return {
|
|
236
|
-
status: "raised",
|
|
237
|
-
lease: current
|
|
238
|
-
};
|
|
239
|
-
return {
|
|
240
|
-
status: "raised",
|
|
241
|
-
lease: writeLease({
|
|
242
|
-
...current,
|
|
243
|
-
expiresAt: new Date(floor).toISOString()
|
|
244
|
-
}, io)
|
|
245
|
-
};
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
function summarize(lease) {
|
|
249
|
-
return {
|
|
250
|
-
platform: lease.platform,
|
|
251
|
-
id: lease.id,
|
|
252
|
-
deviceName: lease.deviceName,
|
|
253
|
-
expiresAt: lease.expiresAt
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
function releaseHeld(root, platform, record, io, slot = "default") {
|
|
257
|
-
const key = deviceSlotKey(platform, slot);
|
|
258
|
-
const path = deviceLeasePath(platform, record.id);
|
|
259
|
-
const released = io.withLeaseLock(deviceLeaseLockPath(platform, record.id), () => {
|
|
260
|
-
const current = parseLease(io.readLease(path));
|
|
261
|
-
if (!current || current.token !== record.token) return null;
|
|
262
|
-
io.removeLease(path);
|
|
263
|
-
return current;
|
|
264
|
-
});
|
|
265
|
-
const held = io.readHolder(root);
|
|
266
|
-
if (held[key]?.token === record.token) {
|
|
267
|
-
delete held[key];
|
|
268
|
-
io.writeHolder(root, held);
|
|
269
|
-
}
|
|
270
|
-
return released;
|
|
271
|
-
}
|
|
272
|
-
function releaseByRoot(root, platform, id, io) {
|
|
273
|
-
const path = deviceLeasePath(platform, id);
|
|
274
|
-
return io.withLeaseLock(deviceLeaseLockPath(platform, id), () => {
|
|
275
|
-
const current = parseLease(io.readLease(path));
|
|
276
|
-
if (!current || current.holder !== root) return null;
|
|
277
|
-
io.removeLease(path);
|
|
278
|
-
return current;
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
function releaseRunLease({ root, platform, slot = "default" }, io = fileLeaseIo) {
|
|
282
|
-
return withHolderLock(io, root, () => {
|
|
283
|
-
const record = io.readHolder(root)[deviceSlotKey(platform, slot)];
|
|
284
|
-
if (!record || record.kind !== "run") return null;
|
|
285
|
-
const released = releaseHeld(root, platform, record, io, slot);
|
|
286
|
-
return released ? summarize(released) : null;
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
function releaseWorkspaceLeases(root, { platform = null, slot = null } = {}, io = fileLeaseIo) {
|
|
290
|
-
return withHolderLock(io, root, () => {
|
|
291
|
-
const released = [];
|
|
292
|
-
for (const [name, record] of Object.entries(io.readHolder(root))) {
|
|
293
|
-
const parsed = parseDeviceSlotKey(name);
|
|
294
|
-
if (!parsed || platform && parsed.platform !== platform || slot !== null && parsed.slot !== slot) continue;
|
|
295
|
-
const lease = releaseHeld(root, parsed.platform, record, io, parsed.slot);
|
|
296
|
-
if (lease) released.push(summarize(lease));
|
|
297
|
-
}
|
|
298
|
-
for (const entry of listLeaseFiles(io)) {
|
|
299
|
-
const lease = entry.lease;
|
|
300
|
-
if (!lease || lease.holder !== root) continue;
|
|
301
|
-
if (platform && lease.platform !== platform) continue;
|
|
302
|
-
if (slot !== null && (lease.slot ?? "default") !== slot) continue;
|
|
303
|
-
const byRoot = releaseByRoot(root, lease.platform, lease.id, io);
|
|
304
|
-
if (byRoot) released.push(summarize(byRoot));
|
|
305
|
-
}
|
|
306
|
-
return released;
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
function removeExpiredLease(entry, io = fileLeaseIo) {
|
|
310
|
-
const lease = entry.lease;
|
|
311
|
-
if (!lease) return false;
|
|
312
|
-
return io.withLeaseLock(deviceLeaseLockPath(lease.platform, lease.id), () => {
|
|
313
|
-
const current = parseLease(io.readLease(entry.path));
|
|
314
|
-
if (!current || !leaseIsExpired(current, io.now())) return false;
|
|
315
|
-
io.removeLease(entry.path);
|
|
316
|
-
return true;
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
function selectPoolDevice({ candidates, leases, held = null, now }) {
|
|
320
|
-
const ordered = candidates.toSorted((a, b) => {
|
|
321
|
-
const left = a.id.toLowerCase();
|
|
322
|
-
const right = b.id.toLowerCase();
|
|
323
|
-
return left < right ? -1 : left > right ? 1 : 0;
|
|
324
|
-
});
|
|
325
|
-
if (held) {
|
|
326
|
-
const mine = ordered.find((candidate) => candidate.id === held);
|
|
327
|
-
return mine ? {
|
|
328
|
-
status: "selected",
|
|
329
|
-
candidate: mine
|
|
330
|
-
} : {
|
|
331
|
-
status: "held-disconnected",
|
|
332
|
-
id: held
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
const preferred = ordered.filter((candidate) => !candidate.fallback);
|
|
336
|
-
const pool = preferred.length > 0 ? preferred : ordered;
|
|
337
|
-
if (pool.length === 0) return { status: "none" };
|
|
338
|
-
const byId = new Map(leases.map((lease) => [lease.id, lease]));
|
|
339
|
-
const free = (candidate) => {
|
|
340
|
-
const lease = byId.get(candidate.id);
|
|
341
|
-
return !lease || leaseIsExpired(lease, now);
|
|
342
|
-
};
|
|
343
|
-
const holders = [];
|
|
344
|
-
for (const candidate of pool) {
|
|
345
|
-
if (free(candidate)) return {
|
|
346
|
-
status: "selected",
|
|
347
|
-
candidate
|
|
348
|
-
};
|
|
349
|
-
const lease = byId.get(candidate.id);
|
|
350
|
-
holders.push({
|
|
351
|
-
id: lease.id,
|
|
352
|
-
holder: lease.holder,
|
|
353
|
-
expiresAt: lease.expiresAt
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
return {
|
|
357
|
-
status: "busy",
|
|
358
|
-
holders,
|
|
359
|
-
spare: ordered.filter((candidate) => candidate.fallback && free(candidate)).map((candidate) => candidate.id)
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
function withHolderLock(io, root, fn) {
|
|
363
|
-
return io.withHolderLock ? io.withHolderLock(root, fn) : fn();
|
|
364
|
-
}
|
|
365
|
-
//#endregion
|
|
366
|
-
export { parseLease as a, releaseRunLease as c, selectPoolDevice as d, takeLease as f, listLeaseFiles as i, releaseWorkspaceLeases as l, fileLeaseIo as n, parseLeaseDuration as o, leaseIsExpired as r, raiseLease as s, deviceLeasePath as t, removeExpiredLease as u };
|