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,636 @@
|
|
|
1
|
+
import { t as getExecutor } from "./exec-CyylIdq9.mjs";
|
|
2
|
+
import { f as getProject, k as withDirLock } from "./config-CMtIaTk9.mjs";
|
|
3
|
+
import { f as readWorkspaceLaunches, p as readWorkspaceState } from "./state-CZ-ER-8d.mjs";
|
|
4
|
+
import { n as deviceConsoleLevel } from "./ios-device-D3pzTKQE.mjs";
|
|
5
|
+
import { i as resolveProjectMetro } from "./metro-COslRK_F.mjs";
|
|
6
|
+
import { n as parseLogcatLine, t as androidClockOffset } from "./android-BW8i_YxU.mjs";
|
|
7
|
+
import { _ as launchErrorPreview } from "./command-output-8wHMEde0.mjs";
|
|
8
|
+
import { a as parseLease, n as fileLeaseIo, t as deviceLeasePath } from "./device-lease-Dpik41RY.mjs";
|
|
9
|
+
import { n as androidHome } from "./android-K63_0UfF.mjs";
|
|
10
|
+
import { c as readLogRecords, t as attachExpoErrorContext } from "./logs-query-oqn47Ffx.mjs";
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
13
|
+
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
//#region src/diagnostic-store.ts
|
|
16
|
+
/** Publishes an immutable diagnostic snapshot atomically; existing evidence wins. */
|
|
17
|
+
function writeDiagnosticOnce(path, content) {
|
|
18
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
19
|
+
withDirLock(`${path}.lock`, () => {
|
|
20
|
+
if (existsSync(path)) return;
|
|
21
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
22
|
+
try {
|
|
23
|
+
writeFileSync(temporary, content, { flag: "wx" });
|
|
24
|
+
renameSync(temporary, path);
|
|
25
|
+
} finally {
|
|
26
|
+
try {
|
|
27
|
+
unlinkSync(temporary);
|
|
28
|
+
} catch {}
|
|
29
|
+
}
|
|
30
|
+
}, {
|
|
31
|
+
waitMs: 100,
|
|
32
|
+
pollMs: 10
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/native-crash.ts
|
|
37
|
+
const IOS_CRASH_REPORT_RETRY = "iOS crash reports can take about a minute or longer to appear. Run `stim logs --errors` again for the native stack.";
|
|
38
|
+
/** Accepts only a report for this app, simulator and launch window. */
|
|
39
|
+
function parseIosCrash(text, target) {
|
|
40
|
+
try {
|
|
41
|
+
const newline = text.indexOf("\n");
|
|
42
|
+
const report = JSON.parse(text.slice(newline + 1));
|
|
43
|
+
const ts = Date.parse(report.captureTime ?? "");
|
|
44
|
+
const simulator = `com.apple.CoreSimulator.SimDevice.${target.deviceId}`;
|
|
45
|
+
if (report.bundleInfo?.CFBundleIdentifier !== target.appId || report.coalitionName !== simulator || !Number.isFinite(ts) || ts < target.since || ts > (target.until ?? Date.now() + 1e3) || !report.incident) return null;
|
|
46
|
+
const stack = ((report.threads?.find((entry) => entry.triggered) ?? report.threads?.[report.faultingThread ?? -1])?.frames ?? []).map((frame) => {
|
|
47
|
+
const image = report.usedImages?.[frame.imageIndex ?? -1];
|
|
48
|
+
return {
|
|
49
|
+
file: frame.sourceFile ?? image?.name ?? "<unknown image>",
|
|
50
|
+
line: frame.sourceLine,
|
|
51
|
+
fn: frame.symbol ? `${frame.symbol}${frame.symbolLocation ? ` + ${frame.symbolLocation}` : ""}` : `+0x${Number(frame.imageOffset ?? 0).toString(16)} [unsymbolicated]`
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
const details = [
|
|
55
|
+
report.exception?.type,
|
|
56
|
+
report.exception?.signal,
|
|
57
|
+
report.termination?.namespace,
|
|
58
|
+
report.termination?.indicator
|
|
59
|
+
].filter(Boolean).join(" / ");
|
|
60
|
+
const reasons = [
|
|
61
|
+
...report.termination?.reasons ?? [],
|
|
62
|
+
...report.termination?.details ?? [],
|
|
63
|
+
...Object.values(report.asi ?? {}).flat()
|
|
64
|
+
].filter((line) => typeof line === "string");
|
|
65
|
+
return {
|
|
66
|
+
report,
|
|
67
|
+
record: {
|
|
68
|
+
ts,
|
|
69
|
+
src: "device",
|
|
70
|
+
platform: "ios",
|
|
71
|
+
level: "fatal",
|
|
72
|
+
event: "native_crash",
|
|
73
|
+
incident: report.incident,
|
|
74
|
+
appId: target.appId,
|
|
75
|
+
deviceId: target.deviceId,
|
|
76
|
+
pid: report.pid,
|
|
77
|
+
msg: `Native crash: ${details || "process terminated"}${reasons.length ? `\n${reasons.join("\n")}` : ""}`,
|
|
78
|
+
stack,
|
|
79
|
+
rawReport: text
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Extracts crash-buffer groups only when their own process header names the target app. */
|
|
87
|
+
function parseAndroidCrashes(text, target, clockOffsetMs) {
|
|
88
|
+
const entries = text.split("\n").map((line) => parseLogcatLine(line, { clockOffsetMs })).filter((record) => record !== null && Number(record.ts) >= target.since && Number(record.ts) <= (target.until ?? Infinity));
|
|
89
|
+
const groups = [];
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
const last = groups.at(-1);
|
|
92
|
+
if (!last || entry.proc !== last.at(-1)?.proc || /^\s*(?:FATAL EXCEPTION:|\*\*\* \*\*\*)/.test(entry.msg ?? "")) groups.push([entry]);
|
|
93
|
+
else last.push(entry);
|
|
94
|
+
}
|
|
95
|
+
return groups.flatMap((group) => {
|
|
96
|
+
const messages = group.map((record) => record.msg ?? "");
|
|
97
|
+
const process = messages.map((line) => /^\s*(?:Process: (.+?), PID: \d+|pid: \d+, tid: \d+, name: .*?>>> (.*?) <<<)/.exec(line)).find(Boolean);
|
|
98
|
+
const appId = process?.[1] ?? process?.[2];
|
|
99
|
+
if (appId !== target.appId) return [];
|
|
100
|
+
const msg = messages.join("\n");
|
|
101
|
+
if (!/FATAL EXCEPTION:|signal \d+ \(/.test(msg)) return [];
|
|
102
|
+
return [{
|
|
103
|
+
ts: group[0].ts,
|
|
104
|
+
deviceTs: group[0].deviceTs,
|
|
105
|
+
pid: Number(/(?:PID: |pid: )(\d+)/.exec(msg)?.[1]) || void 0,
|
|
106
|
+
src: "device",
|
|
107
|
+
platform: "android",
|
|
108
|
+
level: "fatal",
|
|
109
|
+
event: "native_crash",
|
|
110
|
+
appId,
|
|
111
|
+
deviceId: target.deviceId,
|
|
112
|
+
msg: `Native crash (Android)\n${msg}`,
|
|
113
|
+
rawReport: msg
|
|
114
|
+
}];
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function symbolicateIos(record, report, appPath, deadline) {
|
|
118
|
+
if (!appPath || !existsSync(appPath) || !Array.isArray(record.stack)) return record;
|
|
119
|
+
const thread = report.threads?.find((entry) => entry.triggered) ?? report.threads?.[report.faultingThread ?? -1];
|
|
120
|
+
const stack = [...record.stack];
|
|
121
|
+
const verified = /* @__PURE__ */ new Map();
|
|
122
|
+
for (const [index, frame] of (thread?.frames ?? []).entries()) {
|
|
123
|
+
if (Date.now() >= deadline) break;
|
|
124
|
+
if (frame.sourceFile && frame.sourceLine) continue;
|
|
125
|
+
const imageIndex = frame.imageIndex ?? -1;
|
|
126
|
+
const image = report.usedImages?.[imageIndex];
|
|
127
|
+
if (!image?.path || !image.uuid || !image.arch || !Number.isSafeInteger(image.base) || !Number.isSafeInteger(frame.imageOffset)) continue;
|
|
128
|
+
if (!verified.has(imageIndex)) {
|
|
129
|
+
const marker = `${basename(appPath)}/`;
|
|
130
|
+
const at = image.path.lastIndexOf(marker);
|
|
131
|
+
const relative = at < 0 ? null : image.path.slice(at + marker.length);
|
|
132
|
+
const binary = relative ? resolve(appPath, relative) : null;
|
|
133
|
+
let matched = null;
|
|
134
|
+
if (binary && binary.startsWith(resolve(appPath) + sep) && existsSync(binary)) {
|
|
135
|
+
if (getExecutor().runFileQuiet("xcrun", [
|
|
136
|
+
"dwarfdump",
|
|
137
|
+
"--uuid",
|
|
138
|
+
binary
|
|
139
|
+
], { timeoutMs: 2e3 })?.toLowerCase().includes(`uuid: ${image.uuid.toLowerCase()} (${image.arch.toLowerCase()})`)) matched = binary;
|
|
140
|
+
}
|
|
141
|
+
verified.set(imageIndex, matched);
|
|
142
|
+
}
|
|
143
|
+
const binary = verified.get(imageIndex);
|
|
144
|
+
if (!binary || Date.now() >= deadline) continue;
|
|
145
|
+
const address = image.base + frame.imageOffset;
|
|
146
|
+
if (!Number.isSafeInteger(address)) continue;
|
|
147
|
+
const output = getExecutor().runFileQuiet("xcrun", [
|
|
148
|
+
"atos",
|
|
149
|
+
"-o",
|
|
150
|
+
binary,
|
|
151
|
+
"-arch",
|
|
152
|
+
image.arch,
|
|
153
|
+
"-l",
|
|
154
|
+
`0x${image.base.toString(16)}`,
|
|
155
|
+
`0x${address.toString(16)}`
|
|
156
|
+
], { timeoutMs: 2e3 });
|
|
157
|
+
if (output && !/^0x[0-9a-f]+$/i.test(output) && !/error:|cannot|unable/i.test(output)) stack[index] = {
|
|
158
|
+
file: image.name ?? basename(binary),
|
|
159
|
+
fn: output
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
...record,
|
|
164
|
+
stack
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function symbolicateAndroid(record, root, budget) {
|
|
168
|
+
if (!root || typeof record.msg !== "string" || !/#\d+ pc /.test(record.msg)) return record;
|
|
169
|
+
const frames = [...record.msg.matchAll(/^\s*#\d+ pc ([0-9a-f]+)\s+(\S+)(.*)$/gim)];
|
|
170
|
+
const stack = frames.map(([, address, file, suffix]) => ({
|
|
171
|
+
file: basename(file),
|
|
172
|
+
fn: `${suffix?.trim() || `0x${address}`} [captured native frame]`
|
|
173
|
+
}));
|
|
174
|
+
let deadline = Math.min(Date.now() + 2e3, budget - 1e3);
|
|
175
|
+
const ndks = join(androidHome(), "ndk");
|
|
176
|
+
let tools;
|
|
177
|
+
try {
|
|
178
|
+
for (const version of readdirSync(ndks).toSorted().toReversed()) {
|
|
179
|
+
const prebuilt = join(ndks, version, "toolchains/llvm/prebuilt");
|
|
180
|
+
for (const host of readdirSync(prebuilt)) {
|
|
181
|
+
const bin = join(prebuilt, host, "bin");
|
|
182
|
+
if (existsSync(join(bin, "llvm-symbolizer"))) {
|
|
183
|
+
tools = bin;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (tools) break;
|
|
188
|
+
}
|
|
189
|
+
} catch {}
|
|
190
|
+
const files = [];
|
|
191
|
+
const visit = (directory, depth) => {
|
|
192
|
+
if (depth > 10 || files.length > 500 || Date.now() >= deadline) return;
|
|
193
|
+
try {
|
|
194
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) if (entry.isDirectory()) visit(join(directory, entry.name), depth + 1);
|
|
195
|
+
else if (entry.isFile() && entry.name.endsWith(".so")) files.push(join(directory, entry.name));
|
|
196
|
+
} catch {}
|
|
197
|
+
};
|
|
198
|
+
if (tools) {
|
|
199
|
+
for (const directory of [
|
|
200
|
+
"android/app/src/main/jniLibs",
|
|
201
|
+
"android/app/build/intermediates/cxx",
|
|
202
|
+
"android/app/.cxx"
|
|
203
|
+
]) visit(join(root, directory), 0);
|
|
204
|
+
deadline = budget;
|
|
205
|
+
const byId = /* @__PURE__ */ new Map();
|
|
206
|
+
for (const [index, [, address, file, suffix]] of frames.entries()) {
|
|
207
|
+
if (Date.now() >= deadline) break;
|
|
208
|
+
const id = /\(BuildId: ([0-9a-f]+)\)/i.exec(suffix ?? "")?.[1]?.toLowerCase();
|
|
209
|
+
if (!id) continue;
|
|
210
|
+
if (!byId.has(id)) {
|
|
211
|
+
const binary = files.filter((path) => file.endsWith(".apk") || basename(path) === basename(file)).find((path) => {
|
|
212
|
+
if (Date.now() >= deadline) return false;
|
|
213
|
+
const notes = getExecutor().runFileQuiet(join(tools, "llvm-readelf"), ["--notes", path], { timeoutMs: 1e3 });
|
|
214
|
+
return /Build ID: ([0-9a-f]+)/i.exec(notes ?? "")?.[1]?.toLowerCase() === id;
|
|
215
|
+
});
|
|
216
|
+
byId.set(id, binary ?? null);
|
|
217
|
+
}
|
|
218
|
+
const binary = byId.get(id);
|
|
219
|
+
if (!binary) continue;
|
|
220
|
+
const [fn, location] = getExecutor().runFileQuiet(join(tools, "llvm-symbolizer"), [
|
|
221
|
+
"--no-inlines",
|
|
222
|
+
"--demangle",
|
|
223
|
+
`--obj=${binary}`,
|
|
224
|
+
`0x${address}`
|
|
225
|
+
], { timeoutMs: 1e3 })?.split("\n") ?? [];
|
|
226
|
+
const match = /^(.*):(\d+):(\d+)$/.exec(location ?? "");
|
|
227
|
+
if (fn && fn !== "??" && match && match[1] !== "??" && Number(match[2]) > 0) stack[index] = {
|
|
228
|
+
fn,
|
|
229
|
+
file: match[1],
|
|
230
|
+
line: Number(match[2]),
|
|
231
|
+
column: Number(match[3])
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const summary = record.msg.split("\n").filter((line) => /Native crash|signal \d+|Abort message:|^pid:/.test(line)).join("\n");
|
|
236
|
+
return {
|
|
237
|
+
...record,
|
|
238
|
+
msg: summary,
|
|
239
|
+
stack,
|
|
240
|
+
symbolicationNote: stack.some((frame) => !frame.line) ? "Native symbols are partial: unresolved frames retain crash-buffer symbols/addresses. Full captured report: stim logs --source device --json." : void 0
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function simulatorConsolePaths(target, prepare = false, remote = false) {
|
|
244
|
+
if (remote) return void 0;
|
|
245
|
+
const container = getExecutor().runFileQuiet("xcrun", [
|
|
246
|
+
"simctl",
|
|
247
|
+
"get_app_container",
|
|
248
|
+
target.deviceId,
|
|
249
|
+
target.appId,
|
|
250
|
+
"data"
|
|
251
|
+
], { timeoutMs: 2e3 });
|
|
252
|
+
if (!container?.startsWith("/")) return void 0;
|
|
253
|
+
const directory = join(container, "Library/Caches/Stim");
|
|
254
|
+
const key = createHash("sha256").update(JSON.stringify([
|
|
255
|
+
target.deviceId,
|
|
256
|
+
target.appId,
|
|
257
|
+
target.since
|
|
258
|
+
])).digest("hex");
|
|
259
|
+
const paths = {
|
|
260
|
+
stdout: join(directory, `launch-${key}.stdout`),
|
|
261
|
+
stderr: join(directory, `launch-${key}.stderr`)
|
|
262
|
+
};
|
|
263
|
+
if (prepare) try {
|
|
264
|
+
mkdirSync(directory, { recursive: true });
|
|
265
|
+
for (const path of Object.values(paths)) writeFileSync(path, "", { flag: "wx" });
|
|
266
|
+
} catch {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
return paths;
|
|
270
|
+
}
|
|
271
|
+
function simulatorFatalConsole(target) {
|
|
272
|
+
const paths = simulatorConsolePaths(target);
|
|
273
|
+
return Object.values(paths ?? {}).flatMap((path) => {
|
|
274
|
+
try {
|
|
275
|
+
if (statSync(path).size > 8388608) return [];
|
|
276
|
+
if (target.until !== void 0 && statSync(path).mtimeMs > target.until) return [];
|
|
277
|
+
const text = readFileSync(path, "utf8");
|
|
278
|
+
const fatal = text.split("\n").filter((line) => deviceConsoleLevel(line) === "fatal");
|
|
279
|
+
if (!fatal.length) return [];
|
|
280
|
+
return [{
|
|
281
|
+
ts: Math.max(target.since + 1, statSync(path).mtimeMs),
|
|
282
|
+
src: "device",
|
|
283
|
+
platform: "ios",
|
|
284
|
+
level: "fatal",
|
|
285
|
+
event: "native_crash",
|
|
286
|
+
appId: target.appId,
|
|
287
|
+
deviceId: target.deviceId,
|
|
288
|
+
incident: basename(path),
|
|
289
|
+
pid: Number(/\[(\d+):\d+\]/.exec(text)?.[1]) || void 0,
|
|
290
|
+
msg: fatal.join("\n"),
|
|
291
|
+
rawReport: text,
|
|
292
|
+
symbolicationNote: `Captured app stderr; the full operating system crash report is not available yet. ${IOS_CRASH_REPORT_RETRY}`
|
|
293
|
+
}];
|
|
294
|
+
} catch {
|
|
295
|
+
return [];
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
/** Reads platform crash evidence without starting, stopping or modifying a device. */
|
|
300
|
+
function captureNativeCrashes(target, logsDir) {
|
|
301
|
+
let records = [];
|
|
302
|
+
try {
|
|
303
|
+
if (target.platform === "ios" && !target.physical && process.platform === "darwin") {
|
|
304
|
+
const deadline = Date.now() + 4e3;
|
|
305
|
+
records = simulatorFatalConsole(target);
|
|
306
|
+
const directory = join(homedir(), "Library/Logs/DiagnosticReports");
|
|
307
|
+
const nativeReports = readdirSync(directory).filter((name) => name.endsWith(".ips")).map((name) => ({
|
|
308
|
+
path: join(directory, name),
|
|
309
|
+
time: statSync(join(directory, name)).mtimeMs
|
|
310
|
+
})).filter(({ time }) => time >= target.since).toSorted((a, b) => b.time - a.time).slice(0, 50).flatMap(({ path }) => {
|
|
311
|
+
if (Date.now() >= deadline) return [];
|
|
312
|
+
if (statSync(path).size > 8388608) return [];
|
|
313
|
+
const parsed = parseIosCrash(readFileSync(path, "utf8"), target);
|
|
314
|
+
return parsed ? [symbolicateIos(parsed.record, parsed.report, target.appPath, deadline)] : [];
|
|
315
|
+
});
|
|
316
|
+
if (nativeReports.length) records = nativeReports;
|
|
317
|
+
} else if (target.platform === "android") {
|
|
318
|
+
const offset = androidClockOffset(target.deviceId);
|
|
319
|
+
if (offset === null) return [];
|
|
320
|
+
const text = getExecutor().runFileQuiet("adb", [
|
|
321
|
+
"-s",
|
|
322
|
+
target.deviceId,
|
|
323
|
+
"logcat",
|
|
324
|
+
"-b",
|
|
325
|
+
"crash",
|
|
326
|
+
"-d",
|
|
327
|
+
"-v",
|
|
328
|
+
"time,epoch",
|
|
329
|
+
"-t",
|
|
330
|
+
"2000"
|
|
331
|
+
], { timeoutMs: 3e3 });
|
|
332
|
+
const deadline = Date.now() + 4e3;
|
|
333
|
+
if (text) records = parseAndroidCrashes(text, target, offset).map((record) => symbolicateAndroid(record, target.root, deadline));
|
|
334
|
+
}
|
|
335
|
+
} catch {}
|
|
336
|
+
for (const record of records) try {
|
|
337
|
+
const key = createHash("sha256").update(JSON.stringify([
|
|
338
|
+
record.deviceId,
|
|
339
|
+
record.incident ?? record.deviceTs,
|
|
340
|
+
record.rawReport
|
|
341
|
+
])).digest("hex");
|
|
342
|
+
writeDiagnosticOnce(join(logsDir, `native-crash-${key}.ndjson`), `${JSON.stringify(record)}\n`);
|
|
343
|
+
} catch {}
|
|
344
|
+
return records;
|
|
345
|
+
}
|
|
346
|
+
function printNativeCrashReport(target, logsDir, emit, remote) {
|
|
347
|
+
if (remote) return;
|
|
348
|
+
for (const line of launchErrorPreview(captureNativeCrashes(target, logsDir), target.root)) emit(line);
|
|
349
|
+
}
|
|
350
|
+
function captureWorkspaceCrashes(root, logsDir) {
|
|
351
|
+
const attempts = readLogRecords(logsDir).filter((record) => record.event === "launch_attempt");
|
|
352
|
+
const launches = readWorkspaceLaunches(root);
|
|
353
|
+
for (const platform of ["ios", "android"]) try {
|
|
354
|
+
const attempt = attempts.findLast((record) => record.platform === platform);
|
|
355
|
+
if (!attempt || attempt.remote) continue;
|
|
356
|
+
const { deviceId, appId } = attempt;
|
|
357
|
+
const since = attempt.ts;
|
|
358
|
+
const until = Date.now();
|
|
359
|
+
if (typeof deviceId !== "string" || typeof appId !== "string" || typeof since !== "number" || !Number.isFinite(since)) continue;
|
|
360
|
+
if (attempt.physical) {
|
|
361
|
+
const holder = fileLeaseIo.readHolder(root)[platform];
|
|
362
|
+
const lease = parseLease(fileLeaseIo.readLease(deviceLeasePath(platform, deviceId)));
|
|
363
|
+
if (holder?.id !== deviceId || !lease || lease.platform !== platform || lease.id !== deviceId || lease.holder !== root || lease.token !== holder.token || Date.parse(lease.expiresAt) <= until || !lease.grantedAt || !Number.isFinite(Date.parse(lease.grantedAt)) || Date.parse(lease.grantedAt) > since) continue;
|
|
364
|
+
} else {
|
|
365
|
+
const launch = launches[platform];
|
|
366
|
+
if (!launch || launch.deviceId !== deviceId || launch.appId !== appId || Date.parse(launch.launchedAt) !== since) continue;
|
|
367
|
+
const configured = getProject(root)?.platforms?.[platform];
|
|
368
|
+
if (!configured?.owned) continue;
|
|
369
|
+
if (platform === "ios" && configured.deviceUdid !== deviceId) continue;
|
|
370
|
+
if (platform === "android") {
|
|
371
|
+
if (!configured.avdName) continue;
|
|
372
|
+
if (getExecutor().runFileQuiet("adb", [
|
|
373
|
+
"-s",
|
|
374
|
+
deviceId,
|
|
375
|
+
"emu",
|
|
376
|
+
"avd",
|
|
377
|
+
"name"
|
|
378
|
+
], { timeoutMs: 2e3 })?.split("\n")[0]?.trim() !== configured.avdName) continue;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
captureNativeCrashes({
|
|
382
|
+
root,
|
|
383
|
+
platform,
|
|
384
|
+
deviceId,
|
|
385
|
+
appId,
|
|
386
|
+
since: Number(since),
|
|
387
|
+
until,
|
|
388
|
+
physical: attempt?.physical === true,
|
|
389
|
+
appPath: attempt?.appPath ?? readWorkspaceState(root)?.lastBuild?.appPath
|
|
390
|
+
}, logsDir);
|
|
391
|
+
} catch {}
|
|
392
|
+
}
|
|
393
|
+
//#endregion
|
|
394
|
+
//#region src/error-symbolication.ts
|
|
395
|
+
const LOCATION = /https?:\/\/[^\s)'"\\]+?:\d+:\d+/g;
|
|
396
|
+
function frameFromLocation(location) {
|
|
397
|
+
const match = /^(https?:\/\/.+):(\d+):(\d+)$/.exec(location);
|
|
398
|
+
if (!match) return null;
|
|
399
|
+
const file = match[1];
|
|
400
|
+
if (!/\.bundle(?:[/?]|$)/.test(file)) return null;
|
|
401
|
+
const lineNumber = Number(match[2]);
|
|
402
|
+
const column = Number(match[3]) - 1;
|
|
403
|
+
if (!Number.isSafeInteger(lineNumber) || lineNumber < 1 || !Number.isSafeInteger(column) || column < 0) return null;
|
|
404
|
+
return {
|
|
405
|
+
file,
|
|
406
|
+
lineNumber,
|
|
407
|
+
column,
|
|
408
|
+
methodName: "<unknown>"
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
function bundleLocations(record) {
|
|
412
|
+
const found = [...[
|
|
413
|
+
record.msg,
|
|
414
|
+
record.stack,
|
|
415
|
+
record.componentStack
|
|
416
|
+
].filter((value) => typeof value === "string").join("\n").matchAll(LOCATION)].map(([location]) => location);
|
|
417
|
+
for (const value of [record.stack, record.componentStack]) {
|
|
418
|
+
if (!Array.isArray(value)) continue;
|
|
419
|
+
for (const frame of value) if (frame && typeof frame.file === "string" && typeof frame.line === "number" && typeof frame.column === "number") found.push(`${frame.file}:${frame.line}:${frame.column + 1}`);
|
|
420
|
+
}
|
|
421
|
+
return [...new Set(found)].filter((location) => frameFromLocation(location) !== null);
|
|
422
|
+
}
|
|
423
|
+
/** Resolves captured bundle coordinates against the already-verified workspace Metro. */
|
|
424
|
+
async function symbolicateErrors(records, { port, timeoutMs = 2e3, request = fetch }) {
|
|
425
|
+
const candidates = [...new Set(records.flatMap(bundleLocations))].slice(0, 200);
|
|
426
|
+
if (!candidates.length) return [...records];
|
|
427
|
+
const fallback = (reason) => records.map((record) => bundleLocations(record).length ? {
|
|
428
|
+
...record,
|
|
429
|
+
symbolicationNote: `JS symbolication unavailable (${reason}); captured coordinates retained.`
|
|
430
|
+
} : record);
|
|
431
|
+
if (!Number.isInteger(port) || !port || port < 1 || port > 65535) return fallback("no verified Metro");
|
|
432
|
+
try {
|
|
433
|
+
const response = await request(`http://127.0.0.1:${port}/symbolicate`, {
|
|
434
|
+
method: "POST",
|
|
435
|
+
headers: { "Content-Type": "application/json" },
|
|
436
|
+
body: JSON.stringify({ stack: candidates.map(frameFromLocation) }),
|
|
437
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
438
|
+
redirect: "error"
|
|
439
|
+
});
|
|
440
|
+
if (!response.ok) return fallback(`HTTP ${response.status}`);
|
|
441
|
+
const body = await response.json();
|
|
442
|
+
if (!Array.isArray(body.stack) || body.stack.length !== candidates.length) return fallback("invalid Metro response");
|
|
443
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
444
|
+
for (let index = 0; index < candidates.length; index++) {
|
|
445
|
+
const frame = body.stack[index];
|
|
446
|
+
if (!frame || typeof frame !== "object") continue;
|
|
447
|
+
const { file, lineNumber, column } = frame;
|
|
448
|
+
if (typeof file !== "string" || /[\r\n]/.test(file) || /^https?:/.test(file) || !Number.isSafeInteger(lineNumber) || lineNumber < 1 || !Number.isSafeInteger(column) || column < 0) continue;
|
|
449
|
+
resolved.set(candidates[index], `${file}:${lineNumber}:${column + 1}`);
|
|
450
|
+
}
|
|
451
|
+
return records.map((record) => {
|
|
452
|
+
const matches = bundleLocations(record);
|
|
453
|
+
if (!matches.length) return record;
|
|
454
|
+
const replace = (value) => {
|
|
455
|
+
if (typeof value === "string") return value.replace(LOCATION, (location) => resolved.get(location) ?? location);
|
|
456
|
+
if (!Array.isArray(value)) return value;
|
|
457
|
+
return value.map((frame) => {
|
|
458
|
+
if (!frame || typeof frame !== "object") return frame;
|
|
459
|
+
const mapped = resolved.get(`${frame.file}:${frame.line}:${frame.column + 1}`);
|
|
460
|
+
const match = mapped && /^(.*):(\d+):(\d+)$/.exec(mapped);
|
|
461
|
+
return match ? {
|
|
462
|
+
...frame,
|
|
463
|
+
file: match[1],
|
|
464
|
+
line: Number(match[2]),
|
|
465
|
+
column: Number(match[3]) - 1
|
|
466
|
+
} : frame;
|
|
467
|
+
});
|
|
468
|
+
};
|
|
469
|
+
return {
|
|
470
|
+
...record,
|
|
471
|
+
...record.msg !== void 0 ? { msg: replace(record.msg) } : {},
|
|
472
|
+
...record.stack !== void 0 ? { stack: replace(record.stack) } : {},
|
|
473
|
+
...record.componentStack !== void 0 ? { componentStack: replace(record.componentStack) } : {},
|
|
474
|
+
...matches.some((location) => !resolved.has(location)) ? { symbolicationNote: "Some JS frames remain unsymbolicated; captured coordinates retained." } : {}
|
|
475
|
+
};
|
|
476
|
+
});
|
|
477
|
+
} catch {
|
|
478
|
+
return fallback("request failed or timed out");
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
//#endregion
|
|
482
|
+
//#region src/error-diagnostics.ts
|
|
483
|
+
function key(record) {
|
|
484
|
+
return createHash("sha256").update(JSON.stringify(record)).digest("hex");
|
|
485
|
+
}
|
|
486
|
+
function errorTitle(record) {
|
|
487
|
+
const line = String(record.msg ?? "").split("\n")[0].trim().replace(/^ERROR\s+/, "");
|
|
488
|
+
return /^(?:\{\s*)?\[?((?:\w*Error|\w*Exception): .+?)\]?$/.exec(line)?.[1] ?? null;
|
|
489
|
+
}
|
|
490
|
+
function identity(record) {
|
|
491
|
+
return JSON.stringify([
|
|
492
|
+
record.src,
|
|
493
|
+
record.ts,
|
|
494
|
+
record.proc,
|
|
495
|
+
record.msg
|
|
496
|
+
]);
|
|
497
|
+
}
|
|
498
|
+
function attachDeviceErrorContext(timeline, selected) {
|
|
499
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
500
|
+
return selected.map((record) => {
|
|
501
|
+
if (record.src !== "device" || !record.proc) return record;
|
|
502
|
+
if (!errorTitle(record) && !/^\s+at\s+\S/.test(record.msg ?? "")) return record;
|
|
503
|
+
const index = timeline.findIndex((entry) => identity(entry) === identity(record));
|
|
504
|
+
if (index < 0) return record;
|
|
505
|
+
const context = [];
|
|
506
|
+
for (const next of timeline.slice(index + 1)) {
|
|
507
|
+
if (Number(next.ts) - Number(record.ts) > 1e3 || context.length >= 200) break;
|
|
508
|
+
if (next.src !== "device" || next.proc !== record.proc || next.platform !== record.platform) continue;
|
|
509
|
+
if (!/^\s*(?:(?:componentStack|stack):\s*['"]|isComponentError:|[{}],?$|at\s+\S)/.test(next.msg ?? "")) break;
|
|
510
|
+
context.push(String(next.msg));
|
|
511
|
+
consumed.add(identity(next));
|
|
512
|
+
}
|
|
513
|
+
return context.length ? {
|
|
514
|
+
...record,
|
|
515
|
+
msg: [record.msg, ...context].join("\n")
|
|
516
|
+
} : record;
|
|
517
|
+
}).filter((_record, index) => !consumed.has(identity(selected[index])));
|
|
518
|
+
}
|
|
519
|
+
/** Collapses only cross-source copies with the same error title and a shared stack location. */
|
|
520
|
+
function mergeErrorCopies(records, root) {
|
|
521
|
+
const result = [];
|
|
522
|
+
for (const record of records) {
|
|
523
|
+
if (record.event === "native_crash" && record.pid) {
|
|
524
|
+
const duplicate = result.findIndex((other) => other.event === "native_crash" && other.pid === record.pid && other.deviceId === record.deviceId && other.appId === record.appId && Math.abs(Number(other.ts) - Number(record.ts)) < 5e3);
|
|
525
|
+
if (duplicate >= 0) {
|
|
526
|
+
const prior = result[duplicate];
|
|
527
|
+
const full = Array.isArray(record.stack) ? record : prior;
|
|
528
|
+
const consoleRecord = full === record ? prior : record;
|
|
529
|
+
result[duplicate] = {
|
|
530
|
+
...full,
|
|
531
|
+
msg: full.msg === consoleRecord.msg ? full.msg : `${consoleRecord.msg}\n${full.msg}`
|
|
532
|
+
};
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
const title = errorTitle(record);
|
|
537
|
+
const locations = new Set([...JSON.stringify([
|
|
538
|
+
record.msg,
|
|
539
|
+
record.stack,
|
|
540
|
+
record.componentStack
|
|
541
|
+
]).replaceAll(root ? `${root}/` : "\0", "").matchAll(/[^\s()"\\]+:\d+:\d+/g)].map(([location]) => location));
|
|
542
|
+
const existing = title && locations.size ? result.findIndex((other) => !(Array.isArray(other.mirroredSources) ? other.mirroredSources : [other.src]).includes(record.src) && (!other.platform || !record.platform || other.platform === record.platform) && errorTitle(other) === title && typeof other.ts === "number" && typeof record.ts === "number" && Math.abs(other.ts - record.ts) < 1e3 && [...JSON.stringify([
|
|
543
|
+
other.msg,
|
|
544
|
+
other.stack,
|
|
545
|
+
other.componentStack
|
|
546
|
+
]).replaceAll(root ? `${root}/` : "\0", "").matchAll(/[^\s()"\\]+:\d+:\d+/g)].some(([location]) => locations.has(location))) : -1;
|
|
547
|
+
if (existing < 0) result.push(record);
|
|
548
|
+
else {
|
|
549
|
+
const prior = result[existing];
|
|
550
|
+
const richer = JSON.stringify(record).length > JSON.stringify(prior).length ? record : prior;
|
|
551
|
+
const other = richer === record ? prior : record;
|
|
552
|
+
const sources = [.../* @__PURE__ */ new Set([...Array.isArray(prior.mirroredSources) ? prior.mirroredSources : [prior.src], record.src])];
|
|
553
|
+
result[existing] = {
|
|
554
|
+
...richer,
|
|
555
|
+
src: prior.src,
|
|
556
|
+
ts: prior.ts,
|
|
557
|
+
platform: prior.platform ?? record.platform,
|
|
558
|
+
...richer.stack || other.stack ? { stack: richer.stack ?? other.stack } : {},
|
|
559
|
+
...richer.componentStack || other.componentStack ? { componentStack: richer.componentStack ?? other.componentStack } : {},
|
|
560
|
+
msg: /\bCall Stack\b/.test(String(other.msg)) && /componentStack/.test(String(richer.msg)) ? `${other.msg}\n${String(richer.msg).replace(/^[^\n]*\n/, "")}` : richer.msg,
|
|
561
|
+
mirroredSources: sources,
|
|
562
|
+
symbolicationNote: [richer.symbolicationNote, `Same error captured by ${sources.join(" + ")}; raw copies retained in logs --json.`].filter(Boolean).join(" ")
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return result;
|
|
567
|
+
}
|
|
568
|
+
/** Keeps symbolicated launch evidence stable when Metro later rebuilds or stops. */
|
|
569
|
+
async function errorDiagnostics(input, { root, logsDir, port, allowRequest = false }) {
|
|
570
|
+
const timeline = readLogRecords(logsDir);
|
|
571
|
+
const records = attachDeviceErrorContext(timeline, attachExpoErrorContext(timeline, input.map((record) => ({
|
|
572
|
+
...record,
|
|
573
|
+
src: typeof record.src === "string" ? record.src : void 0,
|
|
574
|
+
msg: record.msg == null ? void 0 : String(record.msg),
|
|
575
|
+
level: typeof record.level === "string" ? record.level : void 0,
|
|
576
|
+
ts: typeof record.ts === "number" ? record.ts : void 0
|
|
577
|
+
}))));
|
|
578
|
+
const requestedCount = records.length;
|
|
579
|
+
const related = timeline.filter((candidate) => candidate.src === "device" && records.some((record) => record.src !== "device" && errorTitle(record) && errorTitle(record) === errorTitle(candidate) && Math.abs(Number(record.ts) - Number(candidate.ts)) < 1e3 && (!record.platform || !candidate.platform || record.platform === candidate.platform)) && !records.some((record) => record.src === candidate.src && record.ts === candidate.ts && record.msg === candidate.msg)).slice(-100);
|
|
580
|
+
records.push(...attachDeviceErrorContext(timeline, related));
|
|
581
|
+
const directory = join(logsDir, "error-context");
|
|
582
|
+
const rendered = records.map((record) => {
|
|
583
|
+
try {
|
|
584
|
+
const saved = JSON.parse(readFileSync(join(directory, `${key(record)}.json`), "utf8"));
|
|
585
|
+
return saved.original === key(record) && saved.rendered && typeof saved.rendered === "object" ? saved.rendered : record;
|
|
586
|
+
} catch {
|
|
587
|
+
return record;
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
const pending = records.flatMap((record, index) => {
|
|
591
|
+
if (rendered[index] !== record || !bundleLocations(record).length) return [];
|
|
592
|
+
if (timeline.some((event) => ([
|
|
593
|
+
"bundle_build_started",
|
|
594
|
+
"bundle_build_done",
|
|
595
|
+
"bundle_build_failed",
|
|
596
|
+
"server_started",
|
|
597
|
+
"supervisor_started",
|
|
598
|
+
"bundle_response_started",
|
|
599
|
+
"bundle_response_finished",
|
|
600
|
+
"bundle_response_failed"
|
|
601
|
+
].includes(String(event.event)) || ["expo_stdout", "expo_stderr"].includes(String(event.event)) && /\bBundl(?:ing|ed)\b/.test(event.msg ?? "")) && Number(event.ts) > Number(record.ts) && (!event.platform || !record.platform || event.platform === record.platform))) {
|
|
602
|
+
rendered[index] = {
|
|
603
|
+
...record,
|
|
604
|
+
symbolicationNote: "Metro rebuilt after this error; captured coordinates retained rather than mapping against a different bundle."
|
|
605
|
+
};
|
|
606
|
+
return [];
|
|
607
|
+
}
|
|
608
|
+
return [{
|
|
609
|
+
record,
|
|
610
|
+
index
|
|
611
|
+
}];
|
|
612
|
+
});
|
|
613
|
+
if (pending.length && allowRequest && port && (await resolveProjectMetro(port, root)).metro) {
|
|
614
|
+
const resolved = await symbolicateErrors(pending.map(({ record }) => record), { port });
|
|
615
|
+
for (let index = 0; index < pending.length; index++) {
|
|
616
|
+
const entry = pending[index];
|
|
617
|
+
const value = resolved[index];
|
|
618
|
+
rendered[entry.index] = value;
|
|
619
|
+
if (bundleLocations(value).length === bundleLocations(entry.record).length) continue;
|
|
620
|
+
try {
|
|
621
|
+
writeDiagnosticOnce(join(directory, `${key(entry.record)}.json`), JSON.stringify({
|
|
622
|
+
original: key(entry.record),
|
|
623
|
+
rendered: value
|
|
624
|
+
}));
|
|
625
|
+
} catch {}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
let merged = mergeErrorCopies(rendered.slice(0, requestedCount), root);
|
|
629
|
+
for (const candidate of rendered.slice(requestedCount)) {
|
|
630
|
+
const combined = mergeErrorCopies([...merged, candidate], root);
|
|
631
|
+
if (combined.length === merged.length) merged = combined;
|
|
632
|
+
}
|
|
633
|
+
return merged;
|
|
634
|
+
}
|
|
635
|
+
//#endregion
|
|
636
|
+
export { printNativeCrashReport as a, captureWorkspaceCrashes as i, IOS_CRASH_REPORT_RETRY as n, simulatorConsolePaths as o, captureNativeCrashes as r, errorDiagnostics as t };
|
|
@@ -2,7 +2,7 @@ import { execFileSync, execSync, spawn } from "child_process";
|
|
|
2
2
|
//#region src/exec.ts
|
|
3
3
|
const MAX_BUFFER = 67108864;
|
|
4
4
|
let active = {
|
|
5
|
-
run(cmd, { timeoutMs, cwd } = {}) {
|
|
5
|
+
run(cmd, { timeoutMs, killSignal, cwd } = {}) {
|
|
6
6
|
const opts = {
|
|
7
7
|
encoding: "utf-8",
|
|
8
8
|
stdio: [
|
|
@@ -13,10 +13,11 @@ let active = {
|
|
|
13
13
|
maxBuffer: MAX_BUFFER
|
|
14
14
|
};
|
|
15
15
|
if (timeoutMs) opts.timeout = timeoutMs;
|
|
16
|
+
if (killSignal) opts.killSignal = killSignal;
|
|
16
17
|
if (cwd) opts.cwd = cwd;
|
|
17
18
|
return String(execSync(cmd, opts)).trim();
|
|
18
19
|
},
|
|
19
|
-
runFile(file, args = [], { timeoutMs, cwd, env, omitEnv } = {}) {
|
|
20
|
+
runFile(file, args = [], { timeoutMs, killSignal, cwd, env, omitEnv } = {}) {
|
|
20
21
|
const opts = {
|
|
21
22
|
encoding: "utf-8",
|
|
22
23
|
stdio: [
|
|
@@ -27,6 +28,7 @@ let active = {
|
|
|
27
28
|
maxBuffer: MAX_BUFFER
|
|
28
29
|
};
|
|
29
30
|
if (timeoutMs) opts.timeout = timeoutMs;
|
|
31
|
+
if (killSignal) opts.killSignal = killSignal;
|
|
30
32
|
if (cwd) opts.cwd = cwd;
|
|
31
33
|
if (env || omitEnv?.length) {
|
|
32
34
|
const childEnv = {
|