UNPKG

@licq/nwjs-minidump

Version:

decode a nwjs crashed dump file

481 lines (447 loc) 14.8 kB
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; }