Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions build.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
const fs = require('fs')
const path = require('path')
const {spawnSync} = require('child_process')
const { spawnSync } = require('child_process')

const buildDir = path.join(__dirname, 'build')
if (!fs.existsSync(buildDir)) {
fs.mkdirSync(buildDir, {recursive: true})
fs.mkdirSync(buildDir, { recursive: true })
}
spawnSync(path.join(__dirname, 'deps', 'breakpad', 'configure'), [], {
cwd: buildDir,
Expand Down
177 changes: 177 additions & 0 deletions lib/format.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// Just enough of the minidump format to extract module names + debug
// identifiers so we can download pdbs

const headerMagic = Buffer.from('MDMP').readUInt32LE(0)

if (!Buffer.prototype.readBigUInt64LE) {
Buffer.prototype.readBigUInt64LE = function(offset) {
// ESLint doesn't support BigInt yet
// eslint-disable-next-line
return BigInt(this.readUInt32LE(offset)) + (BigInt(this.readUInt32LE(offset + 4)) << BigInt(32))
}
}

// MDRawHeader
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/google_breakpad/common/minidump_format.h#252
function readHeader (buf) {
return {
signature: buf.readUInt32LE(0),
version: buf.readUInt32LE(4),
stream_count: buf.readUInt32LE(8),
stream_directory_rva: buf.readUInt32LE(12),
checksum: buf.readUInt32LE(16),
time_date_stamp: buf.readUInt32LE(20),
flags: buf.readBigUInt64LE(24)
}
}

// MDRawDirectory
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/google_breakpad/common/minidump_format.h#305
function readDirectory (buf, rva) {
return {
type: buf.readUInt32LE(rva),
location: readLocationDescriptor(buf, rva + 4)
}
}

// MDRawModule
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/google_breakpad/common/minidump_format.h#386
function readRawModule (buf, rva) {
const module = {
base_of_image: buf.readBigUInt64LE(rva),
size_of_image: buf.readUInt32LE(rva + 8),
checksum: buf.readUInt32LE(rva + 12),
time_date_stamp: buf.readUInt32LE(rva + 16),
module_name_rva: buf.readUInt32LE(rva + 20),
version_info: readVersionInfo(buf, rva + 24),
cv_record: readCVRecord(buf, readLocationDescriptor(buf, rva + 24 + 13 * 4)),
misc_record: readLocationDescriptor(buf, rva + 24 + 13 * 4 + 8)
}
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/processor/minidump.cc#2255
module.version = [
module.version_info.file_version_hi >> 16,
module.version_info.file_version_hi & 0xffff,
module.version_info.file_version_lo >> 16,
module.version_info.file_version_lo & 0xffff
].join('.')
module.name = readString(buf, module.module_name_rva)
return module
}

// MDVSFixedFileInfo
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/google_breakpad/common/minidump_format.h#129
function readVersionInfo (buf, base) {
return {
signature: buf.readUInt32LE(base),
struct_version: buf.readUInt32LE(base + 4),
file_version_hi: buf.readUInt32LE(base + 8),
file_version_lo: buf.readUInt32LE(base + 12),
product_version_hi: buf.readUInt32LE(base + 16),
product_version_lo: buf.readUInt32LE(base + 20),
file_flags_mask: buf.readUInt32LE(base + 24),
file_flags: buf.readUInt32LE(base + 28),
file_os: buf.readUInt32LE(base + 32),
file_type: buf.readUInt32LE(base + 24),
file_subtype: buf.readUInt32LE(base + 28),
file_date_hi: buf.readUInt32LE(base + 32),
file_date_lo: buf.readUInt32LE(base + 36)
}
}

// MDLocationDescriptor
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/google_breakpad/common/minidump_format.h#237
function readLocationDescriptor (buf, base) {
return {
data_size: buf.readUInt32LE(base),
rva: buf.readUInt32LE(base + 4)
}
}

// MDGUID
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/google_breakpad/common/minidump_format.h#81
function readGUID (buf) {
return {
data1: buf.readUInt32LE(0),
data2: buf.readUInt16LE(4),
data3: buf.readUInt16LE(6),
data4: [...buf.subarray(8)]
}
}

// guid_and_age_to_debug_id
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/processor/minidump.cc#2153
function debugIdFromGuidAndAge (guid, age) {
return [
guid.data1.toString(16).padStart(8, '0'),
guid.data2.toString(16).padStart(4, '0'),
guid.data3.toString(16).padStart(4, '0'),
...guid.data4.map(x => x.toString(16).padStart(2, '0')),
age.toString(16)
].join('').toUpperCase()
}

// MDCVInfo{PDB70,ELF}
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/google_breakpad/common/minidump_format.h#426
function readCVRecord (buf, { rva, data_size: dataSize }) {
if (rva === 0) return
const cv_signature = buf.readUInt32LE(rva)
if (cv_signature !== 0x53445352 /* SDSR */) {
const age = buf.readUInt32LE(rva + 4 + 16)
const guid = readGUID(buf.subarray(rva + 4, rva + 4 + 16))
return {
cv_signature,
guid,
age,
pdb_file_name: buf.subarray(rva + 4 + 16 + 4, rva + dataSize - 1).toString('utf8'),
debug_file_id: debugIdFromGuidAndAge(guid, age)
}
} else {
return {cv_signature}
}
}

// MDString
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/google_breakpad/common/minidump_format.h#357
function readString (buf, rva) {
if (rva === 0) return null
const bytes = buf.readUInt32LE(rva)
return buf.subarray(rva + 4, rva + 4 + bytes).toString('utf16le')
}

// MDStreamType
// https://chromium.googlesource.com/breakpad/breakpad/+/refs/heads/master/src/google_breakpad/common/minidump_format.h#310
const streamTypes = {
MD_MODULE_LIST_STREAM: 4,
}

const streamTypeProcessors = {
[streamTypes.MD_MODULE_LIST_STREAM]: (stream, buf) => {
const numModules = buf.readUInt32LE(stream.location.rva)
const modules = []
const size = 8 + 4 + 4 + 4 + 4 + 13 * 4 + 8 + 8 + 8 + 8
const base = stream.location.rva + 4
for (let i = 0; i < numModules; i++) {
modules.push(readRawModule(buf, base + i * size))
}
stream.modules = modules
return stream
}
}

module.exports.readMinidump = function readMinidump (buf) {
const header = readHeader(buf)
if (header.signature !== headerMagic) {
throw new Error('not a minidump file')
}

const streams = []
for (let i = 0; i < header.stream_count; i++) {
const stream = readDirectory(buf, header.stream_directory_rva + i * 12)
if (stream.type !== 0) {
streams.push((streamTypeProcessors[stream.type] || (s => s))(stream, buf))
}
}
return { header, streams }
}

module.exports.streamTypes = streamTypes
28 changes: 25 additions & 3 deletions lib/minidump.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
var fs = require('fs')
var path = require('path')
var spawn = require('child_process').spawn
const fs = require('fs')
const path = require('path')
const spawn = require('child_process').spawn
const format = require('./format')

const exe = process.platform === 'win32' ? '.exe' : ''
const commands = {
Expand Down Expand Up @@ -36,6 +37,27 @@ function execute (command, args, callback) {
var globalSymbolPaths = []
module.exports.addSymbolPath = Array.prototype.push.bind(globalSymbolPaths)

module.exports.moduleList = function (minidump, callback) {
fs.readFile(minidump, (err, data) => {
if (err) return callback(err)
const { streams } = format.readMinidump(data)
const moduleList = streams.find(s => s.type === format.streamTypes.MD_MODULE_LIST_STREAM)
if (!moduleList) return callback(new Error('minidump does not contain module list'))
const modules = moduleList.modules.map(m => {
const mod = {
version: m.version,
name: m.name
}
if (m.cv_record) {
mod.pdb_file_name = m.cv_record.pdb_file_name
mod.debug_identifier = m.cv_record.debug_file_id
}
return mod
})
callback(null, modules)
})
}

module.exports.walkStack = function (minidump, symbolPaths, callback, commandArgs) {
if (!callback) {
callback = symbolPaths
Expand Down
42 changes: 40 additions & 2 deletions test/minidump-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,44 @@ describe('minidump', function () {
})
})
})

describe('moduleList()', function () {
describe('on a Linux dump', () => {
it('calls back with a module list', function (done) {
const dumpPath = path.join(__dirname, 'fixtures', 'linux.dmp')
minidump.moduleList(dumpPath, (err, modules) => {
if (err) return done(err)
assert.notEqual(modules.length, 0)
assert(modules.some(m => m.name.endsWith('/electron')))
done()
})
})
})

describe('on a Windows dump', () => {
it('calls back with a module list', function (done) {
const dumpPath = path.join(__dirname, 'fixtures', 'windows.dmp')
minidump.moduleList(dumpPath, (err, modules) => {
if (err) return done(err)
assert.notEqual(modules.length, 0)
assert(modules.some(m => m.name.endsWith('\\electron.exe')))
done()
})
})
})

describe('on a macOS dump', () => {
it('calls back with a module list', function (done) {
const dumpPath = path.join(__dirname, 'fixtures', 'mac.dmp')
minidump.moduleList(dumpPath, (err, modules) => {
if (err) return done(err)
assert.notEqual(modules.length, 0)
assert(modules.some(m => m.name.endsWith('/Electron Helper')))
done()
})
})
})
})
})

var downloadElectron = function (callback) {
Expand All @@ -100,7 +138,7 @@ var downloadElectron = function (callback) {
if (error) return callback(error)

var electronPath = temp.mkdirSync('node-minidump-')
extractZip(zipPath, {dir: electronPath}, function (error) {
extractZip(zipPath, { dir: electronPath }, function (error) {
if (error) return callback(error)

if (process.platform === 'darwin') {
Expand All @@ -123,7 +161,7 @@ var downloadElectronSymbols = function (platform, callback) {
if (error) return callback(error)

var symbolsPath = temp.mkdirSync('node-minidump-')
extractZip(zipPath, {dir: symbolsPath}, function (error) {
extractZip(zipPath, { dir: symbolsPath }, function (error) {
if (error) return callback(error)
callback(null, path.join(symbolsPath, 'electron.breakpad.syms'))
})
Expand Down