stim 1.0.0 → 1.2.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 +29 -10
- package/dist/android-BW8i_YxU.mjs +204 -0
- package/dist/android-Bcmev2s2.mjs +2334 -0
- package/dist/android-K63_0UfF.mjs +727 -0
- package/dist/android-cas-D6HPZ6UQ.mjs +1078 -0
- package/dist/android-cas-compiler.mjs +2 -2
- package/dist/app-install-NFxUahnH.mjs +1149 -0
- package/dist/build-lock-BsVoLDaq.mjs +645 -0
- package/dist/build-slots-D0b6sJyo.mjs +152 -0
- package/dist/{cache-manifest-4tH64LQ9.mjs → cache-manifest-Cu4_Znw9.mjs} +2 -2
- package/dist/cache-manifest.mjs +1 -1
- package/dist/cli.mjs +24 -27166
- package/dist/collector-run.d.mts +1 -0
- package/dist/collector-run.mjs +7 -5
- package/dist/command-output-8wHMEde0.mjs +289 -0
- package/dist/{config-7kmtuhO2.mjs → config-CMtIaTk9.mjs} +44 -13
- package/dist/deps-BLBsTNDS.mjs +435 -0
- package/dist/dev-client-RpvXxCXV.mjs +620 -0
- package/dist/device-CoCPmazS.mjs +250 -0
- package/dist/device-lease-Dpik41RY.mjs +335 -0
- package/dist/device-pool-B5pEVa3t.mjs +408 -0
- package/dist/device-remote-E81KAdXf.mjs +1209 -0
- package/dist/doctor-CqPeMsSm.mjs +1130 -0
- package/dist/doctor-D_CX7ib5.mjs +530 -0
- package/dist/error-diagnostics-Dw8Gn3yw.mjs +636 -0
- package/dist/{exec-bsN9MJXb.mjs → exec-CyylIdq9.mjs} +4 -2
- package/dist/gc-B7KkDinF.mjs +1665 -0
- package/dist/guide-BS2Xc24o.mjs +4503 -0
- package/dist/ios-B7KLgIp5.mjs +2733 -0
- package/dist/ios-D1h1Di39.mjs +451 -0
- package/dist/ios-device-D3pzTKQE.mjs +81 -0
- package/dist/ios-device-xNt0Lg6v.mjs +413 -0
- package/dist/logs-DcX9AvRF.mjs +176 -0
- package/dist/logs-query-oqn47Ffx.mjs +273 -0
- package/dist/metro-COslRK_F.mjs +184 -0
- package/dist/metro-store-CDDZHtPl.mjs +92 -0
- package/dist/ownership-CGPart6T.mjs +60 -0
- package/dist/ownership-CYOyqxUo.mjs +475 -0
- package/dist/ownership-claim-7zRxxTfZ.mjs +569 -0
- package/dist/{project-Dit1CckW.mjs → project-BBTVABUz.mjs} +12 -3
- package/dist/reclaim-C5Jmuahx.mjs +348 -0
- package/dist/reload-Z8nKifAk.mjs +332 -0
- package/dist/remote-cache-DDmJ1Roq.mjs +994 -0
- package/dist/{server-bare-BmTVVmBB.mjs → server-bare-DEk37Mz2.mjs} +8 -2
- package/dist/{server-expo-CHJe1r6j.mjs → server-expo-C3ktAwUG.mjs} +6 -5
- package/dist/server-expo-Drd7ksHu.mjs +2 -0
- package/dist/settings-DwU1_U_q.mjs +708 -0
- package/dist/spawn-entry-nCE6m770.mjs +14 -0
- package/dist/start-BrY4EuIg.mjs +822 -0
- package/dist/{state-2nush3MK.mjs → state-CZ-ER-8d.mjs} +21 -22
- package/dist/stats-BCWEVLrM.mjs +307 -0
- package/dist/stats-BsF15Bti.mjs +62 -0
- package/dist/status-DFI1Hfpg.mjs +393 -0
- package/dist/stop-B7DR4awK.mjs +3 -0
- package/dist/stop-Btg770vV.mjs +591 -0
- package/dist/supervisor-run.d.mts +0 -1
- package/dist/supervisor-run.mjs +4 -4
- package/dist/workspace-process-lock-BKfdys1q.mjs +47 -0
- package/dist/worktree-BPTbI-LN.mjs +1322 -0
- package/dist/worktree-C79xx_kM.mjs +543 -0
- package/dist/xcode-DeKCribl.mjs +1832 -0
- package/package.json +5 -5
- package/shim/bundle-response.cjs +5 -0
- package/dist/android-jc8MnUyL.mjs +0 -514
- package/dist/metro-store-COLl1pOk.mjs +0 -1249
- package/dist/server-expo-BZJm--wA.mjs +0 -2
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { a as parseNdjsonLine, i as levelRank, o as parseNdjsonText } from "./ndjson-DcAtEx_K.mjs";
|
|
2
|
+
import { closeSync, openSync, readFileSync, readSync, readdirSync, statSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { StringDecoder } from "string_decoder";
|
|
5
|
+
//#region src/logs-query.ts
|
|
6
|
+
const SINCE_UNITS = {
|
|
7
|
+
s: 1e3,
|
|
8
|
+
m: 6e4,
|
|
9
|
+
h: 36e5
|
|
10
|
+
};
|
|
11
|
+
const SINCE_FORMS = "30s, 5m, 2h";
|
|
12
|
+
function parseSince(text) {
|
|
13
|
+
if (typeof text !== "string") return { error: `Invalid --since value ${JSON.stringify(text)}. Use a count and a unit, e.g. ${SINCE_FORMS}.` };
|
|
14
|
+
const m = /^(\d+)\s*([smh])$/i.exec(text.trim());
|
|
15
|
+
if (!m) return { error: `Invalid --since value "${text}". Use a count and a unit, e.g. ${SINCE_FORMS}.` };
|
|
16
|
+
const unit = (m[2] ?? "").toLowerCase();
|
|
17
|
+
return { ms: parseInt(m[1] ?? "", 10) * SINCE_UNITS[unit] };
|
|
18
|
+
}
|
|
19
|
+
function compileGrep(pattern) {
|
|
20
|
+
if (pattern instanceof RegExp) return { re: pattern };
|
|
21
|
+
try {
|
|
22
|
+
return { re: new RegExp(String(pattern)) };
|
|
23
|
+
} catch (err) {
|
|
24
|
+
return { error: `Invalid --grep pattern "${pattern}": ${err.message}` };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const ERROR_SOURCES = [
|
|
28
|
+
"metro",
|
|
29
|
+
"client",
|
|
30
|
+
"build"
|
|
31
|
+
];
|
|
32
|
+
function markerWindow(records) {
|
|
33
|
+
let launchTs = null;
|
|
34
|
+
let bundleTs = null;
|
|
35
|
+
for (const r of records) {
|
|
36
|
+
if (r?.marker !== true) continue;
|
|
37
|
+
const ts = tsOf(r);
|
|
38
|
+
if (ts === null) continue;
|
|
39
|
+
if (r.src === "metro") {
|
|
40
|
+
if (bundleTs === null || ts > bundleTs) bundleTs = ts;
|
|
41
|
+
} else if (launchTs === null || ts > launchTs) launchTs = ts;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
launchTs,
|
|
45
|
+
bundleTs
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function recordMatches(record, criteria = {}) {
|
|
49
|
+
if (!record) return false;
|
|
50
|
+
const { sources, minLevel, grep, sinceTs, errorsOnly, markerTs, bundleMarkerTs } = criteria;
|
|
51
|
+
if (sources && sources.length > 0 && !sources.includes(record.src) && !(criteria.includeNativeCrashes && record.src === "device" && record.event === "native_crash")) return false;
|
|
52
|
+
if (minLevel && levelRank(record.level) < levelRank(minLevel)) return false;
|
|
53
|
+
if (errorsOnly) {
|
|
54
|
+
if (record.level !== "error" && record.level !== "fatal") return false;
|
|
55
|
+
if (typeof markerTs === "number") {
|
|
56
|
+
const ts = tsOf(record);
|
|
57
|
+
if (ts === null || ts <= markerTs) return false;
|
|
58
|
+
}
|
|
59
|
+
if (typeof bundleMarkerTs === "number" && record.src === "metro") {
|
|
60
|
+
const ts = tsOf(record);
|
|
61
|
+
if (ts === null || ts < bundleMarkerTs) return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (typeof sinceTs === "number") {
|
|
65
|
+
const ts = tsOf(record);
|
|
66
|
+
if (ts === null || ts < sinceTs) return false;
|
|
67
|
+
}
|
|
68
|
+
if (grep) {
|
|
69
|
+
const re = grep instanceof RegExp ? grep : compileGrep(grep).re;
|
|
70
|
+
if (!re || String(record.msg ?? "").search(re) === -1) return false;
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
function buildCriteria({ sources, minLevel, since, grep, errorsOnly, markerTs, bundleMarkerTs, now } = {}) {
|
|
75
|
+
const criteria = {
|
|
76
|
+
errorsOnly: Boolean(errorsOnly),
|
|
77
|
+
includeNativeCrashes: Boolean(errorsOnly && !sources?.length)
|
|
78
|
+
};
|
|
79
|
+
if (sources && sources.length > 0) criteria.sources = sources;
|
|
80
|
+
else if (criteria.errorsOnly) criteria.sources = ERROR_SOURCES;
|
|
81
|
+
if (minLevel) criteria.minLevel = minLevel;
|
|
82
|
+
if (typeof markerTs === "number") criteria.markerTs = markerTs;
|
|
83
|
+
if (typeof bundleMarkerTs === "number") criteria.bundleMarkerTs = bundleMarkerTs;
|
|
84
|
+
if (since !== void 0 && since !== null && since !== "") {
|
|
85
|
+
const parsed = parseSince(since);
|
|
86
|
+
if (parsed.error) throw new Error(parsed.error);
|
|
87
|
+
criteria.sinceTs = (typeof now === "number" ? now : Date.now()) - parsed.ms;
|
|
88
|
+
}
|
|
89
|
+
if (grep !== void 0 && grep !== null && grep !== "") {
|
|
90
|
+
const compiled = compileGrep(grep);
|
|
91
|
+
if (compiled.error) throw new Error(compiled.error);
|
|
92
|
+
criteria.grep = compiled.re;
|
|
93
|
+
}
|
|
94
|
+
return criteria;
|
|
95
|
+
}
|
|
96
|
+
function logFiles(dir) {
|
|
97
|
+
let entries;
|
|
98
|
+
try {
|
|
99
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
100
|
+
} catch {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
return entries.filter((e) => e.isFile() && e.name.endsWith(".ndjson")).map((e) => e.name).toSorted();
|
|
104
|
+
}
|
|
105
|
+
function fileSizes(dir) {
|
|
106
|
+
const sizes = {};
|
|
107
|
+
for (const name of logFiles(dir)) try {
|
|
108
|
+
sizes[name] = statSync(join(dir, name)).size;
|
|
109
|
+
} catch {}
|
|
110
|
+
return sizes;
|
|
111
|
+
}
|
|
112
|
+
function readLogRecords(dir) {
|
|
113
|
+
const all = [];
|
|
114
|
+
for (const name of logFiles(dir)) {
|
|
115
|
+
let text;
|
|
116
|
+
try {
|
|
117
|
+
text = readFileSync(join(dir, name), "utf-8");
|
|
118
|
+
} catch {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
for (const record of parseNdjsonText(text)) all.push(record);
|
|
122
|
+
}
|
|
123
|
+
return sortByTs(all);
|
|
124
|
+
}
|
|
125
|
+
function isExpoErrorContext(record, event) {
|
|
126
|
+
if (record.src !== "metro" || record.raw !== true || record.event !== event || typeof record.msg !== "string") return false;
|
|
127
|
+
return /^Code: \S/.test(record.msg) || /^\s*>?\s*\d+\s*\|/.test(record.msg) || /^\s*\|\s*\^/.test(record.msg) || /^Call Stack$/.test(record.msg) || /^\s+.+\([^()]+:\d+:\d+\)\s*$/.test(record.msg) || /^\s+at\s+\S+:\d+:\d+\s*$/.test(record.msg);
|
|
128
|
+
}
|
|
129
|
+
function attachExpoErrorContext(all, matched) {
|
|
130
|
+
return matched.map((record) => {
|
|
131
|
+
if (record.src !== "metro" || record.raw !== true || record.level !== "error" && record.level !== "fatal" || typeof record.msg !== "string") return record;
|
|
132
|
+
const index = all.findIndex((entry) => entry.ts === record.ts && entry.src === record.src && entry.msg === record.msg && entry.event === record.event);
|
|
133
|
+
if (index === -1) return record;
|
|
134
|
+
const context = [];
|
|
135
|
+
for (let i = index + 1; i < all.length; i += 1) {
|
|
136
|
+
const next = all[i];
|
|
137
|
+
if (next.src !== "metro") continue;
|
|
138
|
+
if (!isExpoErrorContext(next, record.event)) break;
|
|
139
|
+
context.push(next.msg);
|
|
140
|
+
}
|
|
141
|
+
return context.length > 0 ? {
|
|
142
|
+
...record,
|
|
143
|
+
msg: [record.msg, ...context].join("\n")
|
|
144
|
+
} : record;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function includeBareErrorContext(all, matched, launchTs, sinceTs) {
|
|
148
|
+
if (!matched.some((record) => record.src === "client" && (record.level === "error" || record.level === "fatal"))) return matched;
|
|
149
|
+
const renderedContext = [];
|
|
150
|
+
for (const record of all) {
|
|
151
|
+
if (record.src !== "client" || record.event !== "client_symbolication") continue;
|
|
152
|
+
const ts = tsOf(record);
|
|
153
|
+
if (launchTs !== null && (ts === null || ts <= launchTs)) continue;
|
|
154
|
+
if (sinceTs !== void 0 && (ts === null || ts < sinceTs)) continue;
|
|
155
|
+
renderedContext.push({
|
|
156
|
+
...record,
|
|
157
|
+
msg: `Metro symbolication context (not correlated to a specific client error)\n${String(record.msg ?? "")}`,
|
|
158
|
+
errorContext: true
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return sortByTs([...matched, ...renderedContext]);
|
|
162
|
+
}
|
|
163
|
+
function queryLogs({ dir, sources, minLevel, since, grep, tail, errorsOnly, errorContext, now } = {}) {
|
|
164
|
+
const all = readLogRecords(dir);
|
|
165
|
+
if (all.length === 0) return [];
|
|
166
|
+
const { launchTs, bundleTs } = errorsOnly ? markerWindow(all) : {
|
|
167
|
+
launchTs: null,
|
|
168
|
+
bundleTs: null
|
|
169
|
+
};
|
|
170
|
+
const criteria = buildCriteria({
|
|
171
|
+
sources,
|
|
172
|
+
minLevel,
|
|
173
|
+
since,
|
|
174
|
+
grep,
|
|
175
|
+
errorsOnly,
|
|
176
|
+
now,
|
|
177
|
+
markerTs: launchTs === null ? void 0 : launchTs,
|
|
178
|
+
bundleMarkerTs: bundleTs === null ? void 0 : bundleTs
|
|
179
|
+
});
|
|
180
|
+
let matched = all.filter((r) => recordMatches(r, criteria));
|
|
181
|
+
if (typeof tail === "number" && tail >= 0 && matched.length > tail) matched = matched.slice(matched.length - tail);
|
|
182
|
+
return errorContext ? includeBareErrorContext(all, attachExpoErrorContext(all, matched), launchTs, criteria.sinceTs) : matched;
|
|
183
|
+
}
|
|
184
|
+
function tailRead(prev, size) {
|
|
185
|
+
const state = prev && typeof prev.offset === "number" ? prev : {
|
|
186
|
+
offset: 0,
|
|
187
|
+
partial: ""
|
|
188
|
+
};
|
|
189
|
+
if (size < state.offset) return {
|
|
190
|
+
start: 0,
|
|
191
|
+
prev: {
|
|
192
|
+
offset: 0,
|
|
193
|
+
partial: ""
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
return {
|
|
197
|
+
start: state.offset,
|
|
198
|
+
prev: state
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function advanceTail(prev, chunk, size) {
|
|
202
|
+
const lines = ((prev && typeof prev.partial === "string" ? prev.partial : "") + (chunk || "")).split("\n");
|
|
203
|
+
const partial = lines.pop();
|
|
204
|
+
const records = [];
|
|
205
|
+
for (const line of lines) {
|
|
206
|
+
const record = parseNdjsonLine(line);
|
|
207
|
+
if (record) records.push(record);
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
state: {
|
|
211
|
+
offset: size,
|
|
212
|
+
partial
|
|
213
|
+
},
|
|
214
|
+
records
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function followLogs({ dir, onRecord, criteria = {}, intervalMs = 500, offsets = null }) {
|
|
218
|
+
const state = /* @__PURE__ */ new Map();
|
|
219
|
+
const decoders = /* @__PURE__ */ new Map();
|
|
220
|
+
for (const [name, size] of Object.entries(offsets || fileSizes(dir))) {
|
|
221
|
+
state.set(name, {
|
|
222
|
+
offset: size,
|
|
223
|
+
partial: ""
|
|
224
|
+
});
|
|
225
|
+
decoders.set(name, new StringDecoder("utf8"));
|
|
226
|
+
}
|
|
227
|
+
function pollFile(name) {
|
|
228
|
+
const path = join(dir, name);
|
|
229
|
+
const size = statSync(path).size;
|
|
230
|
+
const entry = state.get(name);
|
|
231
|
+
const { start, prev } = tailRead(entry, size);
|
|
232
|
+
if (prev !== entry) decoders.set(name, new StringDecoder("utf8"));
|
|
233
|
+
if (!decoders.has(name)) decoders.set(name, new StringDecoder("utf8"));
|
|
234
|
+
let chunk = "";
|
|
235
|
+
if (size > start) {
|
|
236
|
+
const fd = openSync(path, "r");
|
|
237
|
+
try {
|
|
238
|
+
const buf = Buffer.allocUnsafe(size - start);
|
|
239
|
+
const read = readSync(fd, buf, 0, size - start, start);
|
|
240
|
+
chunk = decoders.get(name).write(buf.subarray(0, read));
|
|
241
|
+
} finally {
|
|
242
|
+
closeSync(fd);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const next = advanceTail(prev, chunk, size);
|
|
246
|
+
state.set(name, next.state);
|
|
247
|
+
for (const record of next.records) if (recordMatches(record, criteria)) onRecord(record);
|
|
248
|
+
}
|
|
249
|
+
function poll() {
|
|
250
|
+
for (const name of logFiles(dir)) try {
|
|
251
|
+
pollFile(name);
|
|
252
|
+
} catch {}
|
|
253
|
+
}
|
|
254
|
+
const timer = setInterval(poll, intervalMs);
|
|
255
|
+
return function stop() {
|
|
256
|
+
clearInterval(timer);
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function tsOf(record) {
|
|
260
|
+
return typeof record?.ts === "number" && Number.isFinite(record.ts) ? record.ts : null;
|
|
261
|
+
}
|
|
262
|
+
function sortByTs(records) {
|
|
263
|
+
return records.toSorted((a, b) => {
|
|
264
|
+
const ta = tsOf(a);
|
|
265
|
+
const tb = tsOf(b);
|
|
266
|
+
if (ta === null && tb === null) return 0;
|
|
267
|
+
if (ta === null) return 1;
|
|
268
|
+
if (tb === null) return -1;
|
|
269
|
+
return ta - tb;
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
//#endregion
|
|
273
|
+
export { followLogs as a, readLogRecords as c, fileSizes as i, buildCriteria as n, parseSince as o, compileGrep as r, queryLogs as s, attachExpoErrorContext as t };
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { t as getExecutor } from "./exec-CyylIdq9.mjs";
|
|
2
|
+
import { E as inspectProcessIdentity, M as isOnMountedVolume, N as listMountedVolumes, g as loadConfig, n as allMetroPorts, r as claimMetroPort, v as removeProject } from "./config-CMtIaTk9.mjs";
|
|
3
|
+
import { p as readWorkspaceState } from "./state-CZ-ER-8d.mjs";
|
|
4
|
+
import { existsSync, readlinkSync, realpathSync } from "fs";
|
|
5
|
+
import { sep } from "path";
|
|
6
|
+
import { request } from "http";
|
|
7
|
+
import { connect } from "net";
|
|
8
|
+
//#region src/ports.ts
|
|
9
|
+
function isMetroRunning(port) {
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
const req = request({
|
|
12
|
+
hostname: "localhost",
|
|
13
|
+
port,
|
|
14
|
+
path: "/status",
|
|
15
|
+
timeout: 2e3
|
|
16
|
+
}, (res) => {
|
|
17
|
+
let data = "";
|
|
18
|
+
res.on("data", (chunk) => {
|
|
19
|
+
data += chunk;
|
|
20
|
+
});
|
|
21
|
+
res.on("end", () => resolve(data.includes("packager-status:running")));
|
|
22
|
+
});
|
|
23
|
+
req.on("error", () => resolve(false));
|
|
24
|
+
req.on("timeout", () => {
|
|
25
|
+
req.destroy();
|
|
26
|
+
resolve(false);
|
|
27
|
+
});
|
|
28
|
+
req.end();
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function isPortFree(port, { timeoutMs = 400 } = {}) {
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
const sock = connect({
|
|
34
|
+
port,
|
|
35
|
+
host: "127.0.0.1"
|
|
36
|
+
});
|
|
37
|
+
const done = (free) => {
|
|
38
|
+
sock.removeAllListeners();
|
|
39
|
+
sock.destroy();
|
|
40
|
+
resolve(free);
|
|
41
|
+
};
|
|
42
|
+
sock.setTimeout(timeoutMs);
|
|
43
|
+
sock.once("connect", () => done(false));
|
|
44
|
+
sock.once("error", () => done(true));
|
|
45
|
+
sock.once("timeout", () => done(false));
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const FIRST_PORT = 8082;
|
|
49
|
+
async function computeNextPort(isFree = isPortFree) {
|
|
50
|
+
const taken = new Set(allMetroPorts());
|
|
51
|
+
for (let port = FIRST_PORT; port < 8282; port++) {
|
|
52
|
+
if (taken.has(port)) continue;
|
|
53
|
+
if (await isFree(port)) return port;
|
|
54
|
+
}
|
|
55
|
+
throw new Error(`Found no free Metro port between ${FIRST_PORT} and 8281. Free one up, or stop a stale bundler (\`stim status\`, \`stim stop <port>\`).`);
|
|
56
|
+
}
|
|
57
|
+
async function findReclaimablePort(excludeProjectPath, probe = isMetroRunning, { isMounted = isOnMountedVolume, mountedVolumes } = {}) {
|
|
58
|
+
const cfg = loadConfig();
|
|
59
|
+
if (!cfg?.projects) return null;
|
|
60
|
+
const mounted = mountedVolumes || listMountedVolumes();
|
|
61
|
+
const candidates = [];
|
|
62
|
+
for (const [path, proj] of Object.entries(cfg.projects)) {
|
|
63
|
+
if (path === excludeProjectPath) continue;
|
|
64
|
+
if (existsSync(path)) continue;
|
|
65
|
+
if (!isMounted(path, mounted)) continue;
|
|
66
|
+
if (typeof proj.metroPort === "number") candidates.push({
|
|
67
|
+
port: proj.metroPort,
|
|
68
|
+
ownerPath: path
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
for (const c of candidates) if (!await probe(c.port)) return c;
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
async function allocatePort(projectPath, probe = isMetroRunning, isFree = isPortFree) {
|
|
75
|
+
const reclaim = await findReclaimablePort(projectPath, probe);
|
|
76
|
+
if (reclaim && await isFree(reclaim.port)) {
|
|
77
|
+
removeProject(reclaim.ownerPath);
|
|
78
|
+
return reclaim.port;
|
|
79
|
+
}
|
|
80
|
+
return computeNextPort(isFree);
|
|
81
|
+
}
|
|
82
|
+
const RESERVE_ATTEMPTS = 5;
|
|
83
|
+
async function reserveMetroPort(projectPath, probe = isMetroRunning, isFree = isPortFree) {
|
|
84
|
+
for (let attempt = 0; attempt < RESERVE_ATTEMPTS; attempt++) {
|
|
85
|
+
const port = await allocatePort(projectPath, probe, isFree);
|
|
86
|
+
const claimed = claimMetroPort(projectPath, port);
|
|
87
|
+
if (claimed !== null) return claimed;
|
|
88
|
+
}
|
|
89
|
+
throw new Error(`Could not reserve a Metro port after ${RESERVE_ATTEMPTS} attempts: another Stim run claimed each one first. Retry.`);
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/metro.ts
|
|
93
|
+
/**
|
|
94
|
+
* True when some process with this pid exists right now. It cannot tell a recycled pid from the
|
|
95
|
+
* process that first held it, so it is only valid for a pid this run spawned and still holds a
|
|
96
|
+
* handle on. For a pid read from a record on disk, decide with `inspectProcessIdentity` on that
|
|
97
|
+
* record's process token, or take an ownership claim (`ownership-claim.ts`): this answer would keep
|
|
98
|
+
* a dead holder's record looking live until some unrelated process exits.
|
|
99
|
+
*/
|
|
100
|
+
function pidExists(pid) {
|
|
101
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return false;
|
|
102
|
+
try {
|
|
103
|
+
process.kill(pid, 0);
|
|
104
|
+
return true;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return error.code !== "ESRCH";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function parseLsofPids(out) {
|
|
110
|
+
if (!out) return [];
|
|
111
|
+
return String(out).split("\n").map((s) => parseInt(s.trim(), 10)).filter((n) => Number.isFinite(n));
|
|
112
|
+
}
|
|
113
|
+
function parseLsofCwd(out) {
|
|
114
|
+
if (!out) return null;
|
|
115
|
+
const lines = String(out).split("\n");
|
|
116
|
+
const idx = lines.findIndex((l) => l === "fcwd");
|
|
117
|
+
if (idx === -1) return null;
|
|
118
|
+
const nLine = lines.slice(idx + 1).find((l) => l.startsWith("n"));
|
|
119
|
+
return nLine ? nLine.slice(1) : null;
|
|
120
|
+
}
|
|
121
|
+
function processCwd(pid) {
|
|
122
|
+
if (process.platform === "linux") try {
|
|
123
|
+
return readlinkSync(`/proc/${pid}/cwd`);
|
|
124
|
+
} catch {}
|
|
125
|
+
return parseLsofCwd(getExecutor().runQuiet(`lsof -a -p ${pid} -d cwd -Fn`));
|
|
126
|
+
}
|
|
127
|
+
function canonicalPath(path) {
|
|
128
|
+
try {
|
|
129
|
+
return realpathSync(path);
|
|
130
|
+
} catch {
|
|
131
|
+
return path;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function isInsideProject(cwd, projectPath) {
|
|
135
|
+
if (!cwd || !projectPath) return false;
|
|
136
|
+
const a = canonicalPath(cwd);
|
|
137
|
+
const b = canonicalPath(projectPath);
|
|
138
|
+
return a === b || a.startsWith(b.endsWith(sep) ? b : b + sep);
|
|
139
|
+
}
|
|
140
|
+
const NOT_OURS_UNRESPONSIVE = "unresponsive";
|
|
141
|
+
const NOT_OURS_UNREADABLE_CWD = "unreadable-cwd";
|
|
142
|
+
const NOT_OURS_FOREIGN_CWD = "foreign-cwd";
|
|
143
|
+
async function resolveProjectMetro(port, projectPath, { probe = isMetroRunning } = {}) {
|
|
144
|
+
const pid = parseLsofPids(getExecutor().runQuiet(`lsof -nP -iTCP:${port} -sTCP:LISTEN -t`))[0];
|
|
145
|
+
if (pid === void 0) return { missing: true };
|
|
146
|
+
if (!await probe(port)) return {
|
|
147
|
+
notOurs: `pid ${pid} on port ${port} does not answer Metro's /status`,
|
|
148
|
+
kind: NOT_OURS_UNRESPONSIVE,
|
|
149
|
+
pid
|
|
150
|
+
};
|
|
151
|
+
const cwd = processCwd(pid);
|
|
152
|
+
if (!cwd) return {
|
|
153
|
+
notOurs: `pid ${pid} on port ${port}: working directory could not be read`,
|
|
154
|
+
kind: NOT_OURS_UNREADABLE_CWD,
|
|
155
|
+
pid
|
|
156
|
+
};
|
|
157
|
+
if (!isInsideProject(cwd, projectPath)) return {
|
|
158
|
+
notOurs: `pid ${pid} on port ${port} runs from ${cwd}, outside ${projectPath}`,
|
|
159
|
+
kind: NOT_OURS_FOREIGN_CWD,
|
|
160
|
+
pid
|
|
161
|
+
};
|
|
162
|
+
const supervisor = readWorkspaceState(canonicalPath(projectPath))?.supervisor;
|
|
163
|
+
const owned = supervisor?.port === port && inspectProcessIdentity(supervisor) === "same";
|
|
164
|
+
return { metro: {
|
|
165
|
+
pid,
|
|
166
|
+
leader: owned ? supervisor.pid : pid,
|
|
167
|
+
cwd,
|
|
168
|
+
...owned ? { processToken: supervisor.processToken } : {}
|
|
169
|
+
} };
|
|
170
|
+
}
|
|
171
|
+
function killMetroTree(leader, processToken) {
|
|
172
|
+
if (!leader || leader === process.pid || inspectProcessIdentity({
|
|
173
|
+
pid: leader,
|
|
174
|
+
processToken
|
|
175
|
+
}) !== "same") return false;
|
|
176
|
+
try {
|
|
177
|
+
process.kill(-leader, "SIGTERM");
|
|
178
|
+
return true;
|
|
179
|
+
} catch {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
//#endregion
|
|
184
|
+
export { isMetroRunning as a, resolveProjectMetro as i, killMetroTree as n, reserveMetroPort as o, pidExists as r, NOT_OURS_FOREIGN_CWD as t };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { METRO_NAMED_CACHE_LAYOUT, metroCacheRoot, registerCache, sharedStoreRoot, tagSharedStore } from "@stim-cli/core";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
//#region src/supervisor/metro-store.ts
|
|
6
|
+
function metroStoreName(root) {
|
|
7
|
+
try {
|
|
8
|
+
const name = JSON.parse(readFileSync(join(root, "package.json"), "utf-8"))?.name;
|
|
9
|
+
if (typeof name === "string" && name.trim() !== "") return name;
|
|
10
|
+
} catch {}
|
|
11
|
+
return "app";
|
|
12
|
+
}
|
|
13
|
+
function metroStoreRoot(root) {
|
|
14
|
+
return metroCacheRoot(metroStoreName(root));
|
|
15
|
+
}
|
|
16
|
+
function registerMetroStore(storeRoot) {
|
|
17
|
+
const parent = resolve(metroCacheRoot());
|
|
18
|
+
const dir = resolve(storeRoot);
|
|
19
|
+
registerCache({
|
|
20
|
+
dir,
|
|
21
|
+
name: "Metro transform cache",
|
|
22
|
+
prune: "entries",
|
|
23
|
+
entriesDepth: 2,
|
|
24
|
+
layout: METRO_NAMED_CACHE_LAYOUT,
|
|
25
|
+
note: "shared Metro transforms, installed by stim start; no eviction of its own",
|
|
26
|
+
replaces: dir === parent ? [] : [{
|
|
27
|
+
dir: parent,
|
|
28
|
+
name: "Metro transform cache",
|
|
29
|
+
prune: "entries",
|
|
30
|
+
entriesDepth: 2,
|
|
31
|
+
layout: null
|
|
32
|
+
}]
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function hasStoreAt(stores, storeRoot) {
|
|
36
|
+
if (!Array.isArray(stores)) return false;
|
|
37
|
+
return stores.some((s) => sharedStoreRoot(s) === storeRoot);
|
|
38
|
+
}
|
|
39
|
+
function appendCacheStore(config, { storeRoot, FileStore }) {
|
|
40
|
+
if (!config || typeof config !== "object") return {
|
|
41
|
+
added: false,
|
|
42
|
+
storeRoot,
|
|
43
|
+
reason: "no config to add it to"
|
|
44
|
+
};
|
|
45
|
+
const stores = config.cacheStores;
|
|
46
|
+
if (typeof stores === "function") {
|
|
47
|
+
const original = stores;
|
|
48
|
+
config.cacheStores = (cache) => {
|
|
49
|
+
const resolved = original(cache);
|
|
50
|
+
return (Array.isArray(resolved) ? resolved : []).concat([tagSharedStore(new FileStore({ root: storeRoot }), storeRoot)]);
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
added: true,
|
|
54
|
+
storeRoot
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (hasStoreAt(stores, storeRoot)) return {
|
|
58
|
+
added: false,
|
|
59
|
+
storeRoot,
|
|
60
|
+
reason: "a store already points at it"
|
|
61
|
+
};
|
|
62
|
+
config.cacheStores = [...Array.isArray(stores) ? stores : [], tagSharedStore(new FileStore({ root: storeRoot }), storeRoot)];
|
|
63
|
+
return {
|
|
64
|
+
added: true,
|
|
65
|
+
storeRoot
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const EXPO_ADAPTER_FILE = "expo-metro-config.cjs";
|
|
69
|
+
function expoMetroConfigPath(fromUrl = import.meta.url) {
|
|
70
|
+
const here = dirname(fileURLToPath(fromUrl));
|
|
71
|
+
for (const rel of ["../shim", "../../shim"]) {
|
|
72
|
+
const candidate = resolve(here, rel, EXPO_ADAPTER_FILE);
|
|
73
|
+
if (existsSync(candidate)) return candidate;
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const STORE_OK_PREFIX = "stim-metro-store: sharing Metro transforms through ";
|
|
78
|
+
function metroStoreConfirmedRoot(line) {
|
|
79
|
+
if (!line.startsWith(STORE_OK_PREFIX)) return null;
|
|
80
|
+
const root = line.slice(51).trim();
|
|
81
|
+
return root === "" ? null : root;
|
|
82
|
+
}
|
|
83
|
+
function expoMetroStoreEnv({ root, storeRoot, adapterPath, existingOverride }) {
|
|
84
|
+
return {
|
|
85
|
+
EXPO_OVERRIDE_METRO_CONFIG: adapterPath,
|
|
86
|
+
STIM_METRO_STORE: storeRoot,
|
|
87
|
+
STIM_PROJECT_ROOT: root,
|
|
88
|
+
...existingOverride ? { STIM_EXPO_METRO_CONFIG: existingOverride } : {}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
export { metroStoreRoot as a, metroStoreConfirmedRoot as i, expoMetroConfigPath as n, registerMetroStore as o, expoMetroStoreEnv as r, appendCacheStore as t };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { D as sameProcessRecord, E as inspectProcessIdentity } from "./config-CMtIaTk9.mjs";
|
|
2
|
+
import { _ as writeWorkspaceState, m as withWorkspaceStateLock, p as readWorkspaceState } from "./state-CZ-ER-8d.mjs";
|
|
3
|
+
import { r as pidExists } from "./metro-COslRK_F.mjs";
|
|
4
|
+
import { realpathSync } from "node:fs";
|
|
5
|
+
//#region src/collector/state.ts
|
|
6
|
+
function collectorRoot(root) {
|
|
7
|
+
try {
|
|
8
|
+
return realpathSync(root);
|
|
9
|
+
} catch {
|
|
10
|
+
return root;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function registerCollector(root, platform, record) {
|
|
14
|
+
root = collectorRoot(root);
|
|
15
|
+
return withWorkspaceStateLock(root, () => {
|
|
16
|
+
const collectors = {
|
|
17
|
+
...readWorkspaceState(root)?.collectors,
|
|
18
|
+
[platform]: record
|
|
19
|
+
};
|
|
20
|
+
writeWorkspaceState(root, { collectors });
|
|
21
|
+
return collectors;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function unregisterCollector(root, platform, pid, processToken) {
|
|
25
|
+
root = collectorRoot(root);
|
|
26
|
+
return withWorkspaceStateLock(root, () => {
|
|
27
|
+
const collectors = { ...readWorkspaceState(root)?.collectors };
|
|
28
|
+
const record = collectors[platform];
|
|
29
|
+
if (!(platform in collectors) || record?.pid !== pid || record.processToken !== processToken) return collectors;
|
|
30
|
+
delete collectors[platform];
|
|
31
|
+
writeWorkspaceState(root, { collectors: Object.keys(collectors).length ? collectors : void 0 });
|
|
32
|
+
return collectors;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function readCollectors(root) {
|
|
36
|
+
return readWorkspaceState(collectorRoot(root))?.collectors || {};
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/collector/ownership.ts
|
|
40
|
+
function collectorProcessTitle(platform, root) {
|
|
41
|
+
return `stim-collector-${platform} --root ${root}`;
|
|
42
|
+
}
|
|
43
|
+
function verifyCollectorOwnership({ pid, platform, root, isAlive = pidExists, expected }) {
|
|
44
|
+
const record = readCollectors(root)[platform];
|
|
45
|
+
if (expected !== void 0 && !sameProcessRecord(record, expected)) return {
|
|
46
|
+
status: "unverified",
|
|
47
|
+
reason: `the ${platform} collector registration changed during cleanup`
|
|
48
|
+
};
|
|
49
|
+
if (record?.pid === pid) {
|
|
50
|
+
const identity = inspectProcessIdentity(record);
|
|
51
|
+
if (identity === "same") return { status: "ours" };
|
|
52
|
+
if (identity === "gone" || identity === "different") return { status: "gone" };
|
|
53
|
+
}
|
|
54
|
+
return isAlive(pid) ? {
|
|
55
|
+
status: "unverified",
|
|
56
|
+
reason: `pid ${pid} has no verifiable identity recorded for this workspace's ${platform} collector`
|
|
57
|
+
} : { status: "gone" };
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
export { unregisterCollector as a, registerCollector as i, verifyCollectorOwnership as n, readCollectors as r, collectorProcessTitle as t };
|