Skip to content

Commit e6ace9f

Browse files
committed
Enhance crash diagnostics with LLDB integration and recent crash report retrieval
1 parent 5599cd4 commit e6ace9f

File tree

1 file changed

+194
-4
lines changed

1 file changed

+194
-4
lines changed

scripts/metagen.js

Lines changed: 194 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
#!/usr/bin/env node
22

33
const { spawnSync } = require("node:child_process");
4-
const fs = require("node:fs/promises");
4+
const fsp = require("node:fs/promises");
5+
const os = require("node:os");
56
const path = require("node:path");
67

78
const COMMON_FRAMEWORKS = [
@@ -64,6 +65,191 @@ function getSDKPath(platform) {
6465
return output.stdout.trim();
6566
}
6667

68+
function sleep(ms) {
69+
return new Promise((resolve) => setTimeout(resolve, ms));
70+
}
71+
72+
function findLLDB() {
73+
const xcrun = spawnSync("xcrun", ["--find", "lldb"], {
74+
stdio: ["ignore", "pipe", "ignore"],
75+
encoding: "utf8",
76+
});
77+
78+
if (xcrun.status === 0) {
79+
const candidate = xcrun.stdout.trim();
80+
if (candidate) {
81+
return candidate;
82+
}
83+
}
84+
85+
return "lldb";
86+
}
87+
88+
function formatLLDBOutput(output) {
89+
return [output.stdout, output.stderr].filter(Boolean).join("");
90+
}
91+
92+
function runLLDBBacktrace(exec, args) {
93+
const lldb = findLLDB();
94+
const output = spawnSync(
95+
lldb,
96+
[
97+
"--batch",
98+
"--no-lldbinit",
99+
"-o",
100+
"run",
101+
"-k",
102+
"bt",
103+
"-k",
104+
"thread backtrace all",
105+
"--",
106+
exec,
107+
...args,
108+
],
109+
{
110+
stdio: ["ignore", "pipe", "pipe"],
111+
encoding: "utf8",
112+
timeout: 120000,
113+
},
114+
);
115+
116+
return {
117+
output,
118+
};
119+
}
120+
121+
function formatCrashReport(reportPath, content) {
122+
try {
123+
const lines = content.split(/\r?\n/);
124+
const header = JSON.parse(lines[0]);
125+
const body = JSON.parse(lines.slice(1).join("\n"));
126+
const faultingThreadIndex = body.faultingThread ?? 0;
127+
const thread = body.threads?.[faultingThreadIndex];
128+
const usedImages = body.usedImages ?? [];
129+
130+
const imageNames = new Map();
131+
for (const image of usedImages) {
132+
if (typeof image.imageIndex === "number" && image.name) {
133+
imageNames.set(image.imageIndex, image.name);
134+
}
135+
}
136+
137+
const linesOut = [
138+
`Crash report: ${reportPath}`,
139+
`Process: ${body.procName ?? header.app_name ?? "unknown"}`,
140+
`Timestamp: ${header.timestamp ?? body.captureTime ?? "unknown"}`,
141+
`Exception: ${body.exception?.type ?? "unknown"} ${body.exception?.signal ?? ""}`.trim(),
142+
`Termination: ${body.termination?.indicator ?? "unknown"}`,
143+
`Faulting thread: ${faultingThreadIndex}`,
144+
];
145+
146+
const frames = thread?.frames ?? [];
147+
if (frames.length > 0) {
148+
linesOut.push("Faulting thread frames:");
149+
for (const [index, frame] of frames.entries()) {
150+
const image = imageNames.get(frame.imageIndex) ?? `image#${frame.imageIndex ?? "?"}`;
151+
const symbol = frame.symbol ?? "<unknown>";
152+
const offset =
153+
typeof frame.symbolLocation === "number"
154+
? ` + ${frame.symbolLocation}`
155+
: typeof frame.imageOffset === "number"
156+
? ` @ ${frame.imageOffset}`
157+
: "";
158+
linesOut.push(` #${index} ${image} ${symbol}${offset}`);
159+
}
160+
}
161+
162+
return linesOut.join("\n");
163+
} catch (error) {
164+
return [
165+
`Crash report: ${reportPath}`,
166+
"Unable to parse crash report as JSON; showing raw excerpt instead.",
167+
content.slice(0, 4000),
168+
].join("\n");
169+
}
170+
}
171+
172+
async function findRecentCrashReport(exec, startedAtMs) {
173+
const reportsDir = path.join(os.homedir(), "Library", "Logs", "DiagnosticReports");
174+
const execName = path.basename(exec);
175+
const deadline = Date.now() + 10000;
176+
177+
while (Date.now() < deadline) {
178+
let entries = [];
179+
try {
180+
entries = await fsp.readdir(reportsDir, { withFileTypes: true });
181+
} catch {
182+
return null;
183+
}
184+
185+
const candidates = [];
186+
for (const entry of entries) {
187+
if (!entry.isFile() || !entry.name.endsWith(".ips")) {
188+
continue;
189+
}
190+
if (!entry.name.startsWith(execName)) {
191+
continue;
192+
}
193+
194+
const reportPath = path.join(reportsDir, entry.name);
195+
try {
196+
const stat = await fsp.stat(reportPath);
197+
if (stat.mtimeMs + 1000 < startedAtMs) {
198+
continue;
199+
}
200+
candidates.push({ reportPath, mtimeMs: stat.mtimeMs });
201+
} catch {
202+
// Ignore reports that disappear while polling.
203+
}
204+
}
205+
206+
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
207+
if (candidates.length > 0) {
208+
const content = await fsp.readFile(candidates[0].reportPath, "utf8");
209+
return {
210+
reportPath: candidates[0].reportPath,
211+
content,
212+
};
213+
}
214+
215+
await sleep(1000);
216+
}
217+
218+
return null;
219+
}
220+
221+
async function emitCrashDiagnostics(exec, args, startedAtMs) {
222+
console.error("Attempting crash diagnostics...");
223+
224+
const lldbResult = runLLDBBacktrace(exec, args);
225+
const lldbOutput = formatLLDBOutput(lldbResult.output);
226+
227+
if (lldbOutput.trim()) {
228+
console.error("LLDB output:");
229+
console.error(lldbOutput.trimEnd());
230+
} else {
231+
console.error("LLDB produced no output.");
232+
}
233+
234+
const attachDenied =
235+
lldbOutput.includes("Not allowed to attach to process") ||
236+
lldbOutput.includes("attach failed");
237+
if (lldbResult.output.error) {
238+
console.error(`LLDB error: ${lldbResult.output.error.message}`);
239+
}
240+
if (attachDenied) {
241+
console.error("LLDB attach was denied by macOS; falling back to DiagnosticReports.");
242+
}
243+
244+
const crashReport = await findRecentCrashReport(exec, startedAtMs);
245+
if (!crashReport) {
246+
console.error("No recent macOS crash report found in ~/Library/Logs/DiagnosticReports.");
247+
return;
248+
}
249+
250+
console.error(formatCrashReport(crashReport.reportPath, crashReport.content));
251+
}
252+
67253
const sdks = {
68254
macos: {
69255
path: getSDKPath("macosx"),
@@ -126,8 +312,8 @@ async function main() {
126312
}
127313

128314
const typesDir = path.resolve(__dirname, "..", "packages", sdkName, "types");
129-
await fs.rm(typesDir, { recursive: true, force: true });
130-
await fs.mkdir(typesDir, { recursive: true });
315+
await fsp.rm(typesDir, { recursive: true, force: true });
316+
await fsp.mkdir(typesDir, { recursive: true });
131317

132318
for (const arch of Object.keys(sdk.targets)) {
133319
// Use the matching arch binary when available, falling back to arm64.
@@ -154,7 +340,7 @@ async function main() {
154340

155341
let exec;
156342
try {
157-
await fs.access(preferredExec);
343+
await fsp.access(preferredExec);
158344
exec = preferredExec;
159345
} catch {
160346
exec = fallbackExec;
@@ -199,11 +385,15 @@ async function main() {
199385

200386
console.log(`$ MetadataGenerator ${args.join(" ")}`);
201387

388+
const startedAtMs = Date.now();
202389
const output = spawnSync(exec, args, {
203390
stdio: "inherit",
204391
});
205392

206393
if (output.status !== 0) {
394+
if (process.platform === "darwin" && output.signal) {
395+
await emitCrashDiagnostics(exec, args, startedAtMs);
396+
}
207397
console.error(`Failed to generate metadata for ${sdkName} ${arch}`);
208398
console.error(`Command: ${exec} ${args.join(" ")}`);
209399
console.error(`Exit code: ${output.status}`);

0 commit comments

Comments
 (0)