@electron/symbolicate-mac
Version:
Symbolicate textual Electron macOS crashes
207 lines (176 loc) • 7.41 kB
JavaScript
import fs from 'node:fs'
import path from 'node:path'
import { Readable } from 'node:stream'
import { pipeline } from 'node:stream/promises'
import { fileURLToPath } from 'node:url';
import { parseArgs } from 'node:util';
import { createGunzip } from 'node:zlib';
import { symbolicateFrames } from '@indutny/breakpad'
import { parseAddressLine } from './parsing.js'
export const symbolicate = async (options) => {
const {force, file} = options
const cacheDirectory = path.join(path.dirname(fileURLToPath(import.meta.url)), 'cache', 'breakpad_symbols')
const dumpText = await fs.promises.readFile(file, 'utf8')
const images = binaryImages(dumpText)
const electronImage = images.find(v => /electron/i.test(v.library))
if (electronImage) console.error(`Found Electron ${electronImage.version}`)
else console.error('No Electron image found')
const lines = dumpText.split(/\r?\n/);
const linesByImage = new Map();
for (const [lineIndex, line] of lines.entries()) {
const parsedLine = parseAddressLine(line)
if (!parsedLine) {
continue;
}
const library = parsedLine.libraryBaseName || parsedLine.libraryId
const image = images.find(i => i.library === library || i.basename === library)
if (!image) {
continue;
}
const offset = parsedLine.address - image.startAddress
const imageKey = `${image.debugId}/${image.basename}`;
let entry = linesByImage.get(imageKey);
if (!entry) {
entry = { image, group: [] };
linesByImage.set(imageKey, entry);
}
entry.group.push({
image,
offset,
lineIndex,
parsedLine,
});
}
for (const { image, group } of linesByImage.values()) {
const { debugId, basename: moduleBasename, extname: moduleExtname } = image
const suffix = moduleExtname === '.pdb' ? '1' : '0';
const stream = await getSymbolFile(debugId.replace(/-/g, '') + suffix, moduleBasename)
if (!stream) {
continue;
}
const frames = group.map(({ offset }) => offset);
const symbolicated = await symbolicateFrames(stream, frames);
for (const [index, symbol] of symbolicated.entries()) {
if (!symbol) {
continue;
}
const { lineIndex, parsedLine } = group[index];
const line = lines[lineIndex];
lines[lineIndex] = line.substr(0, parsedLine.replace.from) + symbol.name + line.substr(parsedLine.replace.from + parsedLine.replace.length);
}
}
return lines.join('\n')
async function getSymbolFile(moduleId, moduleName) {
const pdb = moduleName.replace(/^\//, '')
const symbolFileName = pdb.replace(/(\.pdb)?$/, '.sym')
const symbolPath = path.join(cacheDirectory, pdb, moduleId, symbolFileName)
if (fs.existsSync(symbolPath) && !force) {
return fs.createReadStream(symbolPath)
}
if (!fs.existsSync(symbolPath) && (!fs.existsSync(path.dirname(symbolPath)) || force)) {
for (const baseUrl of SYMBOL_BASE_URLS) {
if (await fetchSymbol(cacheDirectory, baseUrl, pdb, moduleId, symbolFileName))
return fs.createReadStream(symbolPath)
}
}
}
}
const binaryImages = (dumpText) => {
// e.g.
// 0x109ae8000 - 0x109b37fcf +com.tinyspeck.slackmacgap (4.18.0 - 6748) <AB3CB5D1-EB3F-388F-8C63-416A00DA1AAA> /Applications/Slack.app/Contents/MacOS/Slack
// 0x109b49000 - 0x1115c0f7f +com.github.Electron.framework (13.1.6) <36C7F681-FAD6-3CD1-B327-6C1054C359C7> /Applications/Slack.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework
// 0x112051000 - 0x11205dfff com.apple.StoreKit (1.0 - 459) <3E2D404A-55AA-33F9-9B22-5BA474562E6B> /System/Library/Frameworks/StoreKit.framework/Versions/A/StoreKit
// 0x112073000 - 0x112086ff3 +com.github.Squirrel (1.0 - 1) <68FF73B4-1C5A-3235-B391-3EA358FE89C0> /Applications/Slack.app/Contents/Frameworks/Squirrel.framework/Versions/A/Squirrel
// 0x11209f000 - 0x1120e6fff +com.electron.reactive (3.1.0 - 0.0.0) <5DED8556-18AB-3090-ADBF-AEB05C656853> /Applications/Slack.app/Contents/Frameworks/ReactiveObjC.framework/Versions/A/ReactiveObjC
// 0x10ffeb000 - 0x10fffefff com.tinyspeck.slackmacgap.helper (0) <822C7053-6F03-3481-A781-ACF996BC3C0F> /Applications/Slack.app/Contents/Frameworks/Slack Helper (Renderer).app/Contents/MacOS/Slack Helper (Renderer)
// 0x10c830000 - 0x11583ffff com.github.Electron.framework (*) <4c4c4416-5555-3144-a14d-de8dd5c37e80> /Applications/Slack.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework
const re = /^\s*0x([0-9a-f]+)\s+-\s+0x([0-9a-f]+)\s+(\+)?(\S+)\s+\(([^)]+)\)\s+<([0-9a-f-]+)>\s+(.+)$/mgi
let m
const images = []
while (m = re.exec(dumpText)) {
const [, startAddress, endAddress, plus, library, version, debugId, modulePath] = m
const image = {
startAddress: parseInt(startAddress, 16),
endAddress: parseInt(endAddress, 16),
plus,
library,
version,
debugId,
basename: path.basename(modulePath),
extname: path.extname(modulePath),
}
const existing = images.find(v => v.library === image.library)
if (existing) {
if (existing.debugId !== debugId || existing.startAddress !== image.startAddress) {
console.warn(`Duplicate library entries for ${library}, only using the first.`)
console.warn(existing, image)
}
continue;
}
images.push(image)
}
return images
}
const SYMBOL_BASE_URLS = [
'https://symbols.mozilla.org/try',
'https://symbols.electronjs.org',
]
async function fetchSymbol(directory, baseUrl, pdb, id, symbolFileName) {
const url = `${baseUrl}/${encodeURIComponent(pdb)}/${id}/${encodeURIComponent(symbolFileName)}`
const symbolPath = path.join(directory, pdb, id, symbolFileName)
// ensure path is created
await fs.promises.mkdir(path.dirname(symbolPath), { recursive: true })
const response = await fetch(url, { headers: { 'Accept-Encoding': 'gzip' } })
if (response.ok) {
const readable = Readable.fromWeb(response.body)
const output = fs.createWriteStream(symbolPath)
// create symbol
if (response.headers['content-encoding'] === 'gzip') {
// decompress the gzip
await pipeline(readable, createGunzip(), output)
} else {
await pipeline(readable, output)
}
} else if (response.status === 404) {
return false
} else {
throw new Error(`Response code ${response.status} (${response.statusText})`)
}
return true
}
if ((await fs.promises.realpath(process.argv[1])) === fileURLToPath(import.meta.url)) {
const {
positionals,
values: { force, help, version },
} = parseArgs({
allowPositionals: true,
options: {
force: {
type: 'boolean',
},
help: {
type: 'boolean',
},
version: {
type: 'boolean',
},
},
});
if (positionals.length !== 1 || help) {
console.log(`electron-symbolicate-mac <file>
symbolicate a textual crash dump
Positionals:
file path to crash dump
Options:
--force Redownload symbols if present in cache
--help Show help
--version Show version number`)
process.exit(0)
}
if (version) {
console.log(JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url))).version)
process.exit(0)
}
symbolicate({ file: positionals[0], force }).then(console.log, console.error)
}