@licq/nwjs-minidump
Version:
decode a nwjs crashed dump file
481 lines (447 loc) • 14.8 kB
JavaScript
const dl = require("@licq/dl");
const date = require("@licq/date");
const cp = require("child_process");
const fs = require("fs");
const os = require("os");
const path = require("path");
const _7zipMin = require("7zip-min");
const AdmZip = require("adm-zip");
const Tar = require("tar");
const minidump = require("minidump");
const promisify = require("util").promisify;
const dumpPromisify = promisify(minidump.dump);
const walkStackPromisify = promisify(minidump.walkStack);
const nwBaseUrl = "https://dl.nwjs.io";
const mozillaBaseUrl = "https://symbols.mozilla.org/try";
const symbolsBaseDir = path.join(os.homedir(), ".nwjs-minidump/nw_symbols");
const symbolsTmpDir = path.join(os.tmpdir(), ".nwjs-minidump/nw_symbols");
const dumpTmpDir = path.join(os.tmpdir(), ".nwjs-minidump/nw_symbols");
[symbolsBaseDir, symbolsTmpDir, dumpTmpDir].forEach((dir) => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
});
module.exports = {
getSymbols,
decodeDmp,
};
/**
* 解码dump文件
* @param Object ↓
* @param {pathLike} dumpFile 必填 dmp文件路径
* @param {semverVersion} nwVersion 必填 nwjs版本 eg: 0.57.1 \ 0.57.1-sdk
* @param {pathLike} tagetDumpTxtPath 选填 解码后要保存的路径 不填则直接返回解码后的文本
* @param {pathLike} symbolsPath 选填 使用者提供symbols路径
* @returns
*/
async function decodeDmp({
dumpFile,
nwVersion,
tagetDumpTxtPath,
symbolsPath,
}) {
// support remote url file
if (/^https?:\/\/.*.dmp$/.test(dumpFile)) {
try {
const tmpDumpFile = path.join(dumpTmpDir, path.basename(dumpFile));
if (!fs.existsSync(tmpDumpFile)) {
console.log(`\nstart download file...\n`)
await dl.downloadAsync(dumpFile, {
dir: dumpTmpDir,
target: path.basename(dumpFile),
});
}
dumpFile = tmpDumpFile;
} catch (err) {
console.log(`download ${dumpFile} error:`, err.message);
return;
}
}
if (!dumpFile.endsWith(".dmp") || !fs.existsSync(dumpFile)) {
console.log(
`dmpFile ${dumpFile} does not exist or does not have a .dmp extension.`
);
return;
}
let dumpBaseInfo = {};
if (symbolsPath) {
if (!fs.existsSync(symbolsPath)) {
return console.log(`symbolsPath <${symbolsPath}> not exist.`);
}
dumpBaseInfo.targetSymbolsPath = symbolsPath;
} else {
dumpBaseInfo = await getDumpBaseInfo({ dumpFile, nwVersion });
// 检查下symbols是否存在
if (!fs.existsSync(dumpBaseInfo.targetSymbolsPath)) {
await getSymbols(dumpBaseInfo);
}
}
try {
console.log(`\nstart decodeDmp...\n`);
const report = (
await walkStackPromisify(dumpFile, [dumpBaseInfo.targetSymbolsPath])
).toString();
if (!tagetDumpTxtPath) {
//tagetDumpTxtPath = `${dumpFile}.${date.formatTime("ddhhmmss")}`;
tagetDumpTxtPath = `${dumpFile}.log`;
}
fs.writeFileSync(tagetDumpTxtPath, report);
console.log(`decodeDmp done.\n`);
return report;
} catch (error) {
console.log(`decodeDmp err:`, error);
}
}
/**
* 获取并处理symbol文件
* @param {string} dumpFile 必填 dmp文件路径
* @param {string} nwVersion 必填 nwjs版本 eg: 0.57.1 \ 0.57.1-sdk
* @param {string} tmpDownloadDir 选填 symbols临时下载目录 可选
* @param {string} targetSymbolsBasePath 选填 symbols目标目录 可选
*/
async function getSymbols(dumpBaseInfo) {
if (!dumpBaseInfo.targetSymbolsPath) {
// dumpBaseInfo尚未加工,这里就加工下
dumpBaseInfo = await getDumpBaseInfo(dumpBaseInfo);
}
const dumpModules = await getDumpSymbolModules(dumpBaseInfo);
await fetchSymbols(
dumpModules.filter(([pdb]) => filterSymbols(pdb)),
dumpBaseInfo
);
}
// 过滤一些冗余
function filterSymbols(symbolName) {
return (
![
"nw_elf.dll.pdb",
"nw.dll.pdb",
"nw.exe.pdb",
"ffmpeg.dll.pdb",
"secur32.pdb",
"sxs.pdb",
"Windows.Media.pdb",
"samcli.pdb",
"OneCoreUAPCommonProxyStub.pdb",
"rdpendp.pdb",
"dpapi.pdb",
"msspellcheckingfacility.pdb",
].includes(symbolName) && //win
!symbolName.includes("nwjs") && //mac
!symbolName.includes("\\") //win
);
}
/**
* 获取dump文件所依赖的模块信息 ,用于之后获取对应的symbol文件
* @param {object} dumpBaseInfo
* @returns array
*/
async function getDumpSymbolModules(dumpBaseInfo) {
const modules = [];
try {
let res, regex;
if (dumpBaseInfo.isWinX64) {
res = (await dumpPromisify(dumpBaseInfo.dumpFile)).toString();
// eg: see resources/win-dump-modules
// (debug_file) = 'ffmpeg.dll.pdb'
// (debug_identifier) = '5D66D88BA32E81784C4C44205044422E1'
// (version) = '0.57.1.0'
regex =
/\(debug_file\)\s+= "(?:.+\/)?([^"]+)"\s+\(debug_identifier\)\s+= "([0-9A-F]+)"/gm;
} else {
res = dumpBaseInfo.dumpTxt;
// eg: (WARNING: No symbols, rpcrt4.pdb, C4E0FD0447934EC38FCB2D46A2A4BED21)
regex = /\(WARNING: No symbols,\s+(.*),\s+([0-9A-F]+)\)$/gm;
}
let mod;
while ((mod = regex.exec(res))) {
const [, file, id] = mod;
modules.push([file, id]);
}
} catch (err) {
console.log("\ngetDumpSymbolModules err: ", err.message);
}
if (modules.length) {
console.log(
`\ngetDumpSymbolModules success, modules length: ${modules.length}.\n`
);
} else {
console.log(`\ngetDumpSymbolModules fail.\n`);
process.exit(-1);
}
return modules;
}
/**
* 获取和组装dump文件的基本信息 供后续使用
* @param {string} dumpFile 必填 dmp文件路径
* @param {string} nwVersion 必填 nwjs版本 eg: 0.57.1 \ 0.57.1-sdk
* @param {string} tmpDownloadDir 选填 symbols临时下载目录 可选
* @param {string} targetSymbolsBasePath 选填 symbols目标目录 可选
* @returns object
*/
async function getDumpBaseInfo({
dumpFile,
nwVersion,
tmpDownloadDir = symbolsTmpDir,
targetSymbolsBasePath = symbolsBaseDir,
}) {
try {
const dumpTxt = (await walkStackPromisify(dumpFile)).toString();
let arch = "",
isMacX64,
isMacArm64 = false,
isWinX64 = false;
if (dumpTxt.includes("amd64") && dumpTxt.includes("Windows")) {
arch = "win-x64";
isWinX64 = true;
} else if (dumpTxt.includes("arm64") && dumpTxt.includes("Mac")) {
arch = "osx-arm64";
isMacArm64 = true;
} else if (dumpTxt.includes("amd64") && dumpTxt.includes("Mac")) {
arch = "osx-x64";
isMacX64 = true;
} else {
console.log(`Get dump arch exception: not matched`);
process.exit(-1);
}
return {
arch,
isMacX64,
isMacArm64,
isWinX64,
dumpFile,
dumpTxt,
nwVersion,
tmpDownloadDir,
targetSymbolsPath: path.join(targetSymbolsBasePath, nwVersion, arch),
};
} catch (error) {
console.log("\nGet dump arch error:", error.message);
process.exit(-1);
}
}
/**
* 从远程下载symbols文件
* @param {*} symbolModules
* @param {*} dumpBaseInfo
*/
async function fetchSymbols(symbolModules, dumpBaseInfo) {
console.log(`\nStart get symbols from mozilla server.\n`);
// 并行下载
const res = symbolModules.map(async ([pdb, id]) => {
const symbolFileName = pdb.replace(/(\.pdb)?$/, ".sym");
const symbolPath = path.join(
dumpBaseInfo.targetSymbolsPath,
pdb,
id,
symbolFileName
);
if (!fs.existsSync(symbolPath)) {
try {
return await getSymbolsFromMozilla(
dumpBaseInfo.targetSymbolsPath,
pdb,
id,
symbolFileName
);
} catch (error) {}
}
});
// 确保全部下载执行完成
for (const _promise of res) {
await _promise;
}
console.log(`\nStart get symbols from nwjs server.\n`);
// 从nw资源服务器下载
dumpBaseInfo.isWinX64
? await getSymbolsFromNWForWin(dumpBaseInfo)
: await getSymbolsFromNWForMac(dumpBaseInfo);
console.log(`\ngetSymbolsFromNWServer ok.\n`);
}
/**
* 从mozilla.org获取需要的symbol文件
* @param {string} targetSymbolsPath 下载后的目标目录
* @param {string} pdbName eg: oleacc.pdb(win), libdispatch.dylib(mac)
* @param {string} debugIdentifier eg: 64B399DE2137437F89AFFC621BFDCDFA1
* @param {string} symbolFileName eg: oleacc.sym(win), libdispatch.dylib.sym(mac)
*/
async function getSymbolsFromMozilla(
targetSymbolsPath,
pdbName,
debugIdentifier,
symbolFileName
) {
// eg: https://symbols.mozilla.org/try/oleacc.pdb/64B399DE2137437F89AFFC621BFDCDFA1/oleacc.sym
const url = `${mozillaBaseUrl}/${encodeURIComponent(
pdbName
)}/${debugIdentifier}/${encodeURIComponent(symbolFileName)}`;
const symbolPath = path.join(targetSymbolsPath, pdbName, debugIdentifier);
console.log({ url, symbolPath });
fs.mkdirSync(symbolPath, { recursive: true });
try {
return dl.downloadAsync(url, {
dir: symbolPath,
target: symbolFileName,
});
} catch (err) {
fs.rmdirSync(symbolPath, { recursive: true });
console.log("getSymbolsFromMozilla error:", err.message);
return Promise.resolve();
}
}
/**
* 从nwjs资源服务器获取symbols eg: https://dl.nwjs.io/v0.57.1/nwjs-sdk-symbol-v0.57.1-osx-x64.zip
* @param {*} dumpBaseInfo
*/
async function getSymbolsFromNWForMac(dumpBaseInfo) {
const nwBaseInfo = getNwBaseInfo(dumpBaseInfo);
const tmpDownloadDir = dumpBaseInfo.tmpDownloadDir;
const fileFullPath = path.join(
tmpDownloadDir,
nwBaseInfo.fileFullNameWithSubfix
);
if (!fs.existsSync(fileFullPath)) {
try {
console.log(`\n----先下载包:${nwBaseInfo.fileUrl}\n`);
if (!fs.existsSync(tmpDownloadDir)) {
fs.mkdirSync(tmpDownloadDir, { recursive: true });
}
await dl.downloadAsync(nwBaseInfo.fileUrl, { dir: tmpDownloadDir });
} catch (error) {
return;
}
}
if (!fs.existsSync(fileFullPath)) {
return;
}
// 解压zip
new AdmZip(fileFullPath).extractAllTo(tmpDownloadDir, true);
const _tmpPath = path.join(tmpDownloadDir, nwBaseInfo.fileFullName);
// 解压tar
await Tar.x({
file: path.join(_tmpPath, "nwjs.breakpad.tar"),
cwd: path.join(_tmpPath),
});
fs.rmSync(path.join(_tmpPath, "nwjs.breakpad.tar"));
// 构建目录
fs.readdirSync(_tmpPath)
.filter((f) => f.endsWith(".breakpad"))
.forEach(async (bradkpadFile) => {
const bradkpadFilePath = path.join(_tmpPath, bradkpadFile);
//console.log(`----->bradkpadFilePath:`, bradkpadFilePath)
const bradkpadInfo = await getSymbolNameAndDebugIdentifier(
bradkpadFilePath
);
//console.log(`=====>bradkpadInfo:`, bradkpadInfo)
const bradkpadFileTargetDir = path.join(
dumpBaseInfo.targetSymbolsPath,
bradkpadInfo.symbolName,
bradkpadInfo.debugIdentifier
);
fs.mkdirSync(bradkpadFileTargetDir, { recursive: true });
const targetFilePath = path.join(bradkpadFileTargetDir, bradkpadFile);
fs.copyFileSync(bradkpadFilePath, targetFilePath);
fs.renameSync(
targetFilePath,
path.join(bradkpadFileTargetDir, bradkpadInfo.symbolSymName)
);
});
// fs.rmdirSync(_tmpPath)
}
/**
* 从nwjs资源服务器获取symbols eg: https://dl.nwjs.io/v0.57.1/nwjs-sdk-symbol-v0.57.1-win-x64.7z
* @param {*} dumpBaseInfo
*/
async function getSymbolsFromNWForWin(dumpBaseInfo) {
const nwBaseInfo = getNwBaseInfo(dumpBaseInfo);
const tmpDownloadDir = dumpBaseInfo.tmpDownloadDir;
const fileFullPath = path.join(
tmpDownloadDir,
nwBaseInfo.fileFullNameWithSubfix
);
// eg: /tmp/.nw_symbols/nwjs-sdk-symbol-v0.57.1-osx-x64/nwjs Framework-94.0.4606.81.breakpad
if (!fs.existsSync(fileFullPath)) {
console.log(`\n----先下载包:${nwBaseInfo.fileUrl}\n`);
if (!fs.existsSync(tmpDownloadDir)) {
fs.mkdirSync(tmpDownloadDir, { recursive: true });
}
await dl.downloadAsync(nwBaseInfo.fileUrl, { dir: tmpDownloadDir });
}
const _tmpPath = path.join(tmpDownloadDir, nwBaseInfo.fileFullName);
return new Promise((resolve) => {
// 解压7z
_7zipMin.unpack(fileFullPath, _tmpPath, (err) => {
if (err) console.log(`unpack ${fileFullPath} err:`, err);
// 构建目录
fs.readdirSync(_tmpPath)
// .filter((f) => f.endsWith('.sym'))
.forEach(async (bradkpadFile) => {
const bradkpadFilePath = path.join(_tmpPath, bradkpadFile);
//console.log(`----->bradkpadFilePath:`, bradkpadFilePath)
const bradkpadInfo = await getSymbolNameAndDebugIdentifier(
bradkpadFilePath
);
//console.log(`=====>bradkpadInfo:`, bradkpadInfo)
const bradkpadFileTargetDir = path.join(
dumpBaseInfo.targetSymbolsPath,
bradkpadInfo.symbolName,
bradkpadInfo.debugIdentifier
);
fs.mkdirSync(bradkpadFileTargetDir, { recursive: true });
const targetFilePath = path.join(bradkpadFileTargetDir, bradkpadFile);
fs.copyFileSync(bradkpadFilePath, targetFilePath);
fs.renameSync(
targetFilePath,
path.join(bradkpadFileTargetDir, bradkpadInfo.symbolSymName)
);
});
resolve();
// fs.rmdirSync(_tmpPath)
});
});
}
/**
* 组装nw包的基本信息
* @param {*} dumpBaseInfo
* @returns
*/
function getNwBaseInfo(dumpBaseInfo) {
const nwVersion = dumpBaseInfo.nwVersion.split("-");
// eg: https://dl.nwjs.io/v0.57.1/nwjs-sdk-symbol-v0.57.1-osx-x64.zip
const fileFullName = `nwjs-${nwVersion[1] ? "sdk-" : ""}symbol-v${
nwVersion[0]
}-${dumpBaseInfo.arch}`;
const fileFullNameWithSubfix = `${fileFullName}.${
dumpBaseInfo.isWinX64 ? "7z" : "zip"
}`;
const fileUrl = `${nwBaseUrl}/v${nwVersion[0]}/${fileFullNameWithSubfix}`;
return {
fileFullName,
fileFullNameWithSubfix,
fileUrl,
};
}
/**
* 获取symbol文件的一些基础信息
* @param {*} symbolFile
*/
async function getSymbolNameAndDebugIdentifier(symbolFile) {
const firstLine = fs.readFileSync(symbolFile).toString().split(/\r?\n/)[0];
// * firstLine eg:
// * win: MODULE windows x86_64 32D53C4A340B70364C4C44205044422E1 nw.dll.pad
// * mac: MODULE mac x86_64 87A9EA49BC473F4C8B7817631E820BEB0 nwjs Framework
// console.log(`firstLine:\n`, firstLine);
// 获取DEBUG_IDENTIFIER hash
const debugIdentifier = firstLine.split(" ")[3];
const symbolName = firstLine.split(debugIdentifier)[1].trim();
let symbolSymName = symbolName.endsWith(".pdb")
? symbolName.replace(".pdb", ".sym")
: symbolName + ".sym";
const res = {
debugIdentifier,
symbolName,
symbolSymName,
};
console.log(res);
return res;
}