2024-02-29 20:36:10 +00:00
|
|
|
// Copyright 2024 Signal Messenger, LLC
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
|
|
|
import { tmpdir } from 'os';
|
|
|
|
import { readFile, writeFile, mkdtemp } from 'fs/promises';
|
|
|
|
import { gunzip as gunzipCb } from 'zlib';
|
|
|
|
import { join, basename } from 'path';
|
|
|
|
import { promisify } from 'util';
|
2024-07-11 20:14:10 +00:00
|
|
|
import { symbolicate } from '@indutny/symbolicate-mac';
|
2024-02-29 20:36:10 +00:00
|
|
|
import pMap from 'p-map';
|
|
|
|
|
|
|
|
const gunzip = promisify(gunzipCb);
|
|
|
|
|
|
|
|
const INPUT_FILE = process.argv[2];
|
|
|
|
|
|
|
|
if (!INPUT_FILE) {
|
|
|
|
throw new Error('Usage: node symbolicate-crash-report.js <input>');
|
|
|
|
}
|
|
|
|
|
|
|
|
async function main(): Promise<void> {
|
|
|
|
let file = await readFile(INPUT_FILE);
|
|
|
|
try {
|
|
|
|
file = await gunzip(file);
|
|
|
|
} catch (error) {
|
|
|
|
console.error('Failed to decompress, perhaps file is a plaintext?');
|
|
|
|
}
|
|
|
|
|
|
|
|
const matches = file
|
|
|
|
.toString()
|
|
|
|
.matchAll(
|
|
|
|
/WARN[^\n]*crashReports:\s+dump=\[REDACTED\]([0-9a-z]+).dmp\s+mtime="([\d\-T:.Z]+)"\s+({(\n|.)*?\n})/g
|
|
|
|
);
|
|
|
|
|
|
|
|
function moduleName(filename: string | undefined): string {
|
|
|
|
if (!filename) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
if (filename.startsWith('signal-desktop-')) {
|
|
|
|
return 'electron';
|
|
|
|
}
|
2024-03-05 14:17:00 +00:00
|
|
|
|
|
|
|
if (filename.startsWith('Signal') && filename.endsWith('.exe')) {
|
|
|
|
return 'electron.exe.pdb';
|
|
|
|
}
|
2024-02-29 20:36:10 +00:00
|
|
|
return filename;
|
|
|
|
}
|
|
|
|
|
|
|
|
const dumps = new Array<string>();
|
|
|
|
for (const [, dump, mtime, json] of matches) {
|
|
|
|
const out = [];
|
|
|
|
|
|
|
|
let info;
|
|
|
|
try {
|
|
|
|
info = JSON.parse(json);
|
|
|
|
} catch (error) {
|
|
|
|
console.error('Failed to parse JSON, ignoring', dump, mtime, error);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
out.push(`## dump=${dump} mtime=${mtime}`);
|
|
|
|
out.push('');
|
|
|
|
out.push('```');
|
|
|
|
|
|
|
|
for (const [index, frame] of info.crashing_thread.frames.entries()) {
|
|
|
|
out.push(`${index} ${moduleName(frame.module)} ${frame.offset} () + 0`);
|
|
|
|
}
|
|
|
|
|
|
|
|
out.push('');
|
|
|
|
|
|
|
|
for (const m of info.modules) {
|
|
|
|
const filename = moduleName(m.filename);
|
|
|
|
|
|
|
|
out.push(
|
|
|
|
`${m.base_addr} - ${m.end_addr} ` +
|
|
|
|
`${filename.replace(/\s+/g, '-')} (${m.version}) ` +
|
|
|
|
`<${m.debug_id.slice(0, -1)}> ${filename}`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
out.push('```');
|
|
|
|
|
|
|
|
dumps.push(out.join('\n'));
|
|
|
|
}
|
|
|
|
|
|
|
|
const tmpFolder = await mkdtemp(join(tmpdir(), 'parse-crash-reports'));
|
|
|
|
|
|
|
|
const result = await pMap(
|
|
|
|
dumps,
|
|
|
|
async (text, i) => {
|
|
|
|
const tmpFile = join(tmpFolder, `${i}.txt`);
|
|
|
|
await writeFile(tmpFile, text);
|
|
|
|
|
|
|
|
console.error(`Symbolicating: ${tmpFile}`);
|
|
|
|
return symbolicate({ file: tmpFile });
|
|
|
|
},
|
|
|
|
{ concurrency: 1 }
|
|
|
|
);
|
|
|
|
|
|
|
|
console.log(`# Crash Report ${basename(INPUT_FILE)}`);
|
|
|
|
console.log('');
|
|
|
|
console.log(result.join('\n'));
|
|
|
|
}
|
|
|
|
|
|
|
|
main().catch(error => {
|
|
|
|
console.error(error);
|
|
|
|
process.exit(1);
|
|
|
|
});
|