mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
132 lines
4.8 KiB
JavaScript
132 lines
4.8 KiB
JavaScript
'use strict';
|
|
// Reads the CAUSE out of Crashpad minidumps so a native crash stops being an invisible file.
|
|
//
|
|
// Before this, a main-process SIGSEGV ran none of our JS (uncaughtException is JS-only,
|
|
// child-process-gone is children-only), so the app vanished and left a .dmp nobody read. The boot
|
|
// beacon shipped a lifetime cumulative COUNT, which cannot answer what crashed, when, or during
|
|
// what. This parses the minidump header itself, which is a documented binary format, and reports
|
|
// one record per NEW dump since the last boot.
|
|
//
|
|
// Deliberately parses only the header + stream directory + exception/misc streams. That is enough
|
|
// for cause and timing, costs a few KB of reads, and cannot be confused by a truncated tail.
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const MINIDUMP_MAGIC = 0x504d444d; // 'MDMP'
|
|
const STREAM_EXCEPTION = 6;
|
|
const STREAM_SYSTEM_INFO = 7;
|
|
const STREAM_MISC_INFO = 15;
|
|
|
|
// Mach exception codes; the signal is what a user-facing report should say.
|
|
const MAC_EXC = {
|
|
1: 'EXC_BAD_ACCESS (SIGSEGV/SIGBUS)',
|
|
2: 'EXC_BAD_INSTRUCTION (SIGILL)',
|
|
3: 'EXC_ARITHMETIC',
|
|
5: 'EXC_BREAKPOINT (SIGTRAP)',
|
|
6: 'EXC_SOFTWARE',
|
|
10: 'EXC_CRASH (SIGABRT)',
|
|
};
|
|
|
|
function p_readStreams(fd, size) {
|
|
const head = Buffer.alloc(32);
|
|
fs.readSync(fd, head, 0, 32, 0);
|
|
if (head.readUInt32LE(0) !== MINIDUMP_MAGIC) return null;
|
|
const streamCount = head.readUInt32LE(8);
|
|
const streamRva = head.readUInt32LE(12);
|
|
const timeDateStamp = head.readUInt32LE(20);
|
|
if (streamCount > 4096 || streamRva + streamCount * 12 > size) return null;
|
|
const dir = Buffer.alloc(streamCount * 12);
|
|
fs.readSync(fd, dir, 0, dir.length, streamRva);
|
|
const streams = new Map();
|
|
for (let i = 0; i < streamCount; i++) {
|
|
const off = i * 12;
|
|
streams.set(dir.readUInt32LE(off), {
|
|
size: dir.readUInt32LE(off + 4),
|
|
rva: dir.readUInt32LE(off + 8),
|
|
});
|
|
}
|
|
return { streams, timeDateStamp };
|
|
}
|
|
|
|
function p_readExceptionStream(fd, s, fileSize) {
|
|
if (!s || s.rva + 24 > fileSize) return null;
|
|
const buf = Buffer.alloc(Math.min(s.size, 168));
|
|
fs.readSync(fd, buf, 0, buf.length, s.rva);
|
|
// MINIDUMP_EXCEPTION_STREAM: ThreadId(4) __align(4) then MINIDUMP_EXCEPTION
|
|
const threadId = buf.readUInt32LE(0);
|
|
const code = buf.readUInt32LE(8);
|
|
const flags = buf.readUInt32LE(12);
|
|
// ExceptionAddress is 8 bytes at offset 24 within the exception record
|
|
let address = 0n;
|
|
try { address = buf.readBigUInt64LE(24); } catch (_) { address = 0n; }
|
|
return { threadId, code, flags, address: '0x' + address.toString(16) };
|
|
}
|
|
|
|
/** Parse one minidump for cause + timing. Returns null if the file is not a readable minidump. */
|
|
function readDump(file) {
|
|
let fd = null;
|
|
try {
|
|
const st = fs.statSync(file);
|
|
fd = fs.openSync(file, 'r');
|
|
const parsed = p_readStreams(fd, st.size);
|
|
if (!parsed) return null;
|
|
const exc = p_readExceptionStream(fd, parsed.streams.get(STREAM_EXCEPTION), st.size);
|
|
const crashedAt = parsed.timeDateStamp ? new Date(parsed.timeDateStamp * 1000).toISOString() : null;
|
|
return {
|
|
file: path.basename(file),
|
|
bytes: st.size,
|
|
crashed_at: crashedAt || new Date(st.mtimeMs).toISOString(),
|
|
mtime_ms: st.mtimeMs,
|
|
has_exception_stream: !!exc,
|
|
exception_code: exc ? exc.code : null,
|
|
exception_name: exc ? (MAC_EXC[exc.code] || `code ${exc.code}`) : null,
|
|
exception_address: exc ? exc.address : null,
|
|
faulting_thread_id: exc ? exc.threadId : null,
|
|
has_system_info: parsed.streams.has(STREAM_SYSTEM_INFO),
|
|
has_misc_info: parsed.streams.has(STREAM_MISC_INFO),
|
|
};
|
|
} catch (_) {
|
|
return null;
|
|
} finally {
|
|
if (fd !== null) { try { fs.closeSync(fd); } catch (_) {} }
|
|
}
|
|
}
|
|
|
|
/** Every .dmp under a Crashpad dir, newest first. */
|
|
function listDumps(crashpadDir) {
|
|
const out = [];
|
|
const walk = (d) => {
|
|
let entries = [];
|
|
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch (_) { return; }
|
|
for (const e of entries) {
|
|
const p = path.join(d, e.name);
|
|
if (e.isDirectory()) walk(p);
|
|
else if (/\.dmp$/i.test(e.name)) out.push(p);
|
|
}
|
|
};
|
|
walk(crashpadDir);
|
|
return out.sort((a, b) => {
|
|
try { return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs; } catch (_) { return 0; }
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Dumps written since `sinceMs`, parsed. `sinceMs` is the previous boot's watermark, so a relaunch
|
|
* reports only what actually happened while the user was away, not the lifetime pile.
|
|
*/
|
|
function newDumpsSince(crashpadDir, sinceMs, limit = 10) {
|
|
const rows = [];
|
|
for (const f of listDumps(crashpadDir)) {
|
|
let mt = 0;
|
|
try { mt = fs.statSync(f).mtimeMs; } catch (_) { continue; }
|
|
if (mt <= sinceMs) break;
|
|
const parsed = readDump(f);
|
|
if (parsed) rows.push(parsed);
|
|
if (rows.length >= limit) break;
|
|
}
|
|
return rows;
|
|
}
|
|
|
|
module.exports = { readDump, listDumps, newDumpsSince, MINIDUMP_MAGIC };
|