likes
comments
collection
share

Electron 程序崩溃分析

作者站长头像
站长
· 阅读数 47

1 收集及上传崩溃报告

官方文档:www.electronjs.org/zh/docs/lat…

2 breakpad 分析 dmp 文件

官网:chromium.googlesource.com/breakpad/br…

说明下,这里我使用第一步 “git clone chromium.googlesource.com/breakpad/br…” 时就卡住了,直接下载超时,使用VPN也不行,后面手动下载后又发现执行不了命令 "./configure",我是windows系统下操作的,“./configure && make” 命令是 linux 下的?breakpad不是跨平台的吗?后面就放弃了,转用 winDbg 了。有兴趣的可以自行查看官网研究下。

3 winDbg 分析 dmp 文件

winDbg 文档:learn.microsoft.com/zh-cn/windo…

下载地址:learn.microsoft.com/zh-cn/windo…

详细步骤如下

3.1 准备好收集的 .dmp 文件

参考 第1小节。

3.2 设置符号表路径

对应 Electron 开发的应用程序来说,一般只需要以下三种 PDB 符号文件:

  1. Microsoft PDB 符号服务器:msdl.microsoft.com/download/sy…
  2. Electron PDB 符号服务器:symbols.electronjs.org
  3. 应用程序 PDB 符号(本地 PDB 文件路径

目前我程序本地的 pdb 文件尚未生成,在 electron-builder 配置项中配置了 includePdb 选项还是不行, 还没找到解决办法,如果有知道的小伙伴希望不吝赐教!

Electron 程序崩溃分析

Electron 程序崩溃分析

  • SRV*D:\symbols*msdl.microsoft.com/download/sy…;

    这个目录是Microsoft PDB 符号服务器路径,后面的链接表示没有的话去微软官网下载。

  • SRV*D:\symbols*symbols.electronjs.org

    这个目录是 Electron PDB 符号服务器路径,后面的链接表示没有的话去Electron官网下载。

如果本地有 .pdb 文件,可以继续添加到路径后面,用分号隔开即可(例如:C:\Users\DELL\Desktop\iMonitor-CS\dist_electron\Debug)。

你可以在你的电脑中使用任何可写的目录来代替 D:\symbols。复制时请注意上方用到了转义符 \。

3.3 dmp文件导入

Electron 程序崩溃分析

选择要解析的 .dmp 文件即可。

3.4 命令分析dump文件

Electron 程序崩溃分析

使用 !analyze -v 命令来分析dmp文件。按下回车后输入框左侧变成 BUSY 状态,表示正在分析中了,耐心等待结果即可。

Electron 程序崩溃分析

分析完后的内容如下,不知道是不是没有本地 PDB 文件的原因,看不出什么东西,定位不到 js 层面的源码。

0:000> !analyze -v
*******************************************************************************
*                                                                             *
*                        Exception Analysis                                   *
*                                                                             *
*******************************************************************************

*** WARNING: Unable to verify checksum for KERNEL32.DLL
*** WARNING: Unable to verify checksum for USER32.dll

FAULTING_IP: 
KERNELBASE!RaiseException+62
761fa6e2 8b4c2454        mov     ecx,dword ptr [esp+54h]

EXCEPTION_RECORD:  ffffffff -- (.exr 0xffffffffffffffff)
ExceptionAddress: 761fa6e2 (KERNELBASE!RaiseException+0x00000062)
   ExceptionCode: e0000008
  ExceptionFlags: 00000001
NumberParameters: 1
   Parameter[0]: 00000000

BUGCHECK_STR:  e0000008

DEFAULT_BUCKET_ID:  APPLICATION_FAULT

PROCESS_NAME:  iMonitor-CS.exe

ERROR_CODE: (NTSTATUS) 0xe0000008 - <Unable to get error code text>

EXCEPTION_CODE: (NTSTATUS) 0xe0000008 - <Unable to get error code text>

EXCEPTION_PARAMETER1:  00000000

NTGLOBALFLAG:  0

FAULTING_THREAD:  00001394

LAST_CONTROL_TRANSFER:  from 02af3a7f to 761fa6e2

STACK_TEXT:  
0011c964 02af3a7f e0000008 00000001 00000001 KERNELBASE!RaiseException+0x62
WARNING: Stack unwind information not available. Following frames may be wrong.
0011c984 01ea576f 00000000 0011c998 01ea5558 iMonitor_CS!node::tracing::TraceEventHelper::GetCategoryGroupEnabled+0x20d0cf
0011c990 01ea5558 0011c9b4 014f61a0 061b8954 iMonitor_CS!uv_handle_set_data+0x1e7bdf
0011c998 014f61a0 061b8954 00000001 0011edf3 iMonitor_CS!uv_handle_set_data+0x1e79c8
0011c9b4 014f6172 006949a0 061b8954 00000001 iMonitor_CS!v8::V8::GetVersion+0x250
0011f0d4 01660f34 006949a0 061b8954 00000001 iMonitor_CS!v8::V8::GetVersion+0x222
0011f0e8 0168923f 061b8954 0000002c 19002300 iMonitor_CS!cppgc::internal::GlobalGCInfoTable::GCInfoFromIndex+0x2a974
0011f1e4 016866d8 00699628 0011f200 00699628 iMonitor_CS!cppgc::internal::GlobalGCInfoTable::GCInfoFromIndex+0x52c7f
0011f200 01661480 00000900 00000000 00699628 iMonitor_CS!cppgc::internal::GlobalGCInfoTable::GCInfoFromIndex+0x50118
0011f238 0165ec3f 00699628 0011f2d0 fd977d32 iMonitor_CS!cppgc::internal::GlobalGCInfoTable::GCInfoFromIndex+0x2aec0
0011f308 0165c44e 00000001 0165c42e 00000001 iMonitor_CS!cppgc::internal::GlobalGCInfoTable::GCInfoFromIndex+0x2867f
0011f434 01665130 00000001 00000009 00000040 iMonitor_CS!cppgc::internal::GlobalGCInfoTable::GCInfoFromIndex+0x25e8e
0011f454 01671575 00000009 00699628 0068a118 iMonitor_CS!cppgc::internal::GlobalGCInfoTable::GCInfoFromIndex+0x2eb70
0011f4b4 02b0c4ac 1a8ba4d8 006ba818 0011f500 iMonitor_CS!cppgc::internal::GlobalGCInfoTable::GCInfoFromIndex+0x3afb5
0011f548 03313cac 0658d506 006ba818 00000000 iMonitor_CS!node::tracing::TraceEventHelper::GetCategoryGroupEnabled+0x225afc
0011f620 033137fd 0011f648 0011f650 ffffffff iMonitor_CS!uv_gettimeofday+0x3fe0c
0011f694 03303c08 0011f6b0 0111f6c0 006325c8 iMonitor_CS!uv_gettimeofday+0x3f95d
0011f6e4 0331437e 00656a1c ffffffff 00000000 iMonitor_CS!uv_gettimeofday+0x2fd68
0011f720 02af745d 00000001 ffffffff 7fffffff iMonitor_CS!uv_gettimeofday+0x404de
0011f778 031a2aa9 ffffff01 00667b60 0011f780 iMonitor_CS!node::tracing::TraceEventHelper::GetCategoryGroupEnabled+0x210aad
0011f82c 01fb8045 0011f864 0811f878 0060e218 iMonitor_CS!IsSandboxedProcess+0x206d49
0011f848 01fb86d8 0011f878 0011f864 0011f9f0 iMonitor_CS!uv_handle_set_data+0x2fa4b5
0011f894 0143e861 00000000 005f0000 007f0c94 iMonitor_CS!uv_handle_set_data+0x2fab48
0011f994 0143eb1f 0011f9d0 0060e218 0060e218 iMonitor_CS!uv_backend_fd+0x2f651
0011f9b4 0091143c 0011f9d0 0011f9fc 00637960 iMonitor_CS!uv_backend_fd+0x2f90f
0011fa68 05ee382a 00910000 00000000 005f1ee0 iMonitor_CS!Ordinal0+0x143c
0011fab4 7677fa29 002dc000 7677fa10 0011fb20 iMonitor_CS!uv_random+0xf0abfa
0011fac4 77877a4e 002dc000 79b8aadb 00000000 KERNEL32!BaseThreadInitThunk+0x19
0011fb20 77877a1e ffffffff 77898928 00000000 ntdll!__RtlUserThreadStart+0x2f
0011fb30 00000000 05ee38b0 002dc000 00000000 ntdll!_RtlUserThreadStart+0x1b


STACK_COMMAND:  ~0s; .ecxr ; kb

FOLLOWUP_IP: 
iMonitor_CS!node::tracing::TraceEventHelper::GetCategoryGroupEnabled+20d0cf
02af3a7f 56              push    esi

SYMBOL_STACK_INDEX:  1

SYMBOL_NAME:  iMonitor_CS!node::tracing::TraceEventHelper::GetCategoryGroupEnabled+20d0cf

FOLLOWUP_NAME:  MachineOwner

MODULE_NAME: iMonitor_CS

IMAGE_NAME:  iMonitor-CS.exe

DEBUG_FLR_IMAGE_TIMESTAMP:  612e4d9f

BUCKET_ID:  e0000008_iMonitor_CS!node::tracing::TraceEventHelper::GetCategoryGroupEnabled+20d0cf

PRIMARY_PROBLEM_CLASS:  APPLICATION_FAULT

FAILURE_BUCKET_ID:  APPLICATION_FAULT_e0000008_iMonitor-CS.exe!node::tracing::TraceEventHelper::GetCategoryGroupEnabled

Followup: MachineOwner
---------

在生成 .dmp 的时间点在主进程中也监听到了 render-process-gone 事件的触发,同时捕捉到的日志为:details:{"reason":"oom"}`,为内存溢出(out of memory) .dmp 文件保存在:app.getPath( 'crashDumps') 路径下,app为主进程中的模块。

Electron 崩溃相关文章参考:

www.cnblogs.com/zwj-1993062…

4 曲线救国:崩溃事件监听+错误日志

因为我做的第二款应用是 基于 Electron框架 及 canvas 动画,持续运行的 监控软件,且测试时堆内存运行是平稳的,崩溃原因难以定位。客户那边崩溃是偶发的,且是内网,不能上传崩溃文件,只能自行去拷贝,通过 .dmp 文件来分析又只能分析到 js 层面的东西,所以采用了一些曲线救国的方式来排查、解决问题,并尽可能多记录一些错误日志。

4.1 如何记录日志

可以通过下面的 saveLog 方法记录相关错误日志:

getPath 方法用来设置保存路径。

const { app,BrowserWindow } = require('electron')
const fs = require('fs')
const path = require('path')

function getPath() {
    let filePath
    // 开发环境
    if (process.env.NODE_ENV === 'development') {
        filePath = 'C:/Users/DELL/Desktop/data'
        // 生产环境
    } else {
        filePath = path.join(app.getAppPath(), '../../data')
    }
    return filePath
}

function writeFile(savePath, data) {
    fs.appendFile(savePath, data, (err) => {
        // 失败
        if (err) {
            // 向渲染进程发送消息通知失败
            console.log(err)
            BrowserWindow.getFocusedWindow() && BrowserWindow.getFocusedWindow().webContents.send('monitorMainError', err)
        } else {
            BrowserWindow.getFocusedWindow() && BrowserWindow.getFocusedWindow().webContents.send('monitorMainError', '日志写入成功')
        }
    })
}

function saveLog(data) {
    let filePath = getPath()
    let fileName = 'error.log'
    let writePath = `${filePath}\${fileName}`
    if (fs.existsSync(filePath)) {
        writeFile(writePath,data)
    } else {
        fs.mkdir(filePath, { recursive: true }, (err) => {
            if (err) {
                throw err
            } else {
                // console.log('data文件夹创建成功');
                fs.writeFile(writePath, data, (err) => {
                    if (err) {
                        console.log(err)
                    }
                })
            }
        })
    }
}

module.exports = { saveLog }

使用时引入即可:

const { saveLog } = require('./main_tools.js')
...
// 记录 crashReporter 的日志路径:app.getPath('crashDumps'),方便查找
saveLog(
    `dumps文件拷贝目录: ${app.getPath(
        'crashDumps'
    )}\n编译版本:2024-01-31\n启动时间:${new Date().toLocaleString()}\n\n`
)

也可以采用主进程事件监听的方式记录:

function getPath() {
    let filePath
    // 开发环境
    if (process.env.NODE_ENV === 'development') {
        filePath = 'C:/Users/DELL/Desktop/data'
        // 生产环境
    } else {
        filePath = path.join(app.getAppPath(), '../../data')
    }
    return filePath
}

// 写文件,追加内容
function appendFile(filePath, writePath, data, e) {
    if (fs.existsSync(writePath)) {
        fs.appendFile(writePath, data, (err) => {
            // 失败
            if (err) {
                // 向渲染进程发送消息通知失败
                e.reply('monitorMainError', err)
            } else {
            }
        })
    } else {
        fs.mkdir(filePath, { recursive: true }, (err) => {
            if (err) {
                throw err
            } else {
                // console.log('data文件夹创建成功');
                fs.writeFile(writePath, data, (err) => {
                    if (err) {
                        console.log(err)
                    }
                })
            }
        })
    }
}

// 保存日志
// data: 记录的日志数据
ipcMain.on('saveLog', (e, data) => {
    let filePath = getPath()
    let fileName = 'error.log'
    let writePath = `${filePath}\${fileName}`

    appendFile(filePath, writePath, data, e)
})

通过 ipcRenderer 模块使用:

const { ipcRenderer } = require('electron')
...
// 请求拦截器
axios.interceptors.request.use(
    (config) => {
        ...
    },
    (error) => {
        let errText = JSON.stringify(error)
        errText += `\ntype: requestError\ndate: ${new Date().toLocaleString()}`
        ipcRenderer.send('saveLog', `${errText}\n\n`)
        ...
    }
)

4.2 崩溃事件监听

程序崩溃时通过 render-process-gonechild-process-gone 事件来监听。并记录下相应的日志:

   import { app, BrowserWindow } from 'electron'
   
   let win = null
   ...
   
function recordProcessInfo() {
    let cpuUsage = process.cpuUsage() // cpu 使用情况
    let heapStatistics = process.getHeapStatistics() // 返回包含 V8 堆统计的对象。 备注:所有数据值以KB为单位(下同)
    let blinkMemoryInfo = process.getBlinkMemoryInfo() // 返回带有Blink内存信息的对象。 可以用于调试渲染/DOM相关内存问题。
    let processMemoryInfo = process.getProcessMemoryInfo() // 返回一个对象,提供当前进程的内存使用统计。
    let systemMemoryInfo = process.getSystemMemoryInfo() // 返回一个对象,提供整个系统的内存使用统计
    let systemVersion = process.getSystemVersion() // 返回 string - 操作系统的版本。

    if (typeof cpuUsage === 'object') {
        cpuUsage = JSON.stringify(cpuUsage)
    }
    if (typeof heapStatistics === 'object') {
        heapStatistics = JSON.stringify(heapStatistics)
    }
    if (typeof blinkMemoryInfo === 'object') {
        blinkMemoryInfo = JSON.stringify(blinkMemoryInfo)
    }
    if (typeof processMemoryInfo === 'object') {
        processMemoryInfo = JSON.stringify(processMemoryInfo)
    }
    if (typeof systemMemoryInfo === 'object') {
        systemMemoryInfo = JSON.stringify(systemMemoryInfo)
    }
    if (typeof systemVersion === 'object') {
        systemVersion = JSON.stringify(systemVersion)
    }

    saveLog(`cpuUsage: ${cpuUsage} \n`)
    saveLog(`heapStatistics: ${heapStatistics} \n`)
    saveLog(`blinkMemoryInfo: ${blinkMemoryInfo} \n`)
    saveLog(`processMemoryInfo: ${processMemoryInfo} \n`)
    saveLog(`systemMemoryInfo: ${systemMemoryInfo} \n`)
    saveLog(`systemVersion: ${systemVersion} \n`)
}

function crashedActions({ type, details, reloadDelay = 2000 }) {
    saveLog(
        `触发时间: ${new Date().toLocaleString()} \n '${type} 触发--> details:' ${details} \n`
    )
    recordProcessInfo()

    // 渲染进程崩溃后,2s后刷新当前页面
    setTimeout(() => {
        // 调用当前窗口的 win.webContents.reload() 方法来重新加载当前窗口所绑定的网页。
        win.webContents.reload()
        saveLog(
            `win.webContents.reload() 被触发时间: ${new Date().toLocaleString()}\n\n`
        )
    }, reloadDelay)
}

async function createWindow() {
    win = new BrowserWindow({
        ...
    })

   // 渲染器进程意外消失时触发。 这种情况通常因为进程崩溃或被杀死。
   win.webContents.on('render-process-gone', (event, details) => {
        if (typeof details === 'object') {
            details = JSON.stringify(details)
        }
        crashedActions({ type: 'render-process-gone', details })
    })
}
    
app.on('child-process-gone', (event, details) => {
    if (typeof details === 'object') {
        details = JSON.stringify(details)
    }

    crashedActions({ type: 'child-process-gone', details })
})

4.3 Vue 层面错误日志捕获

可以写在了 main.js 中:

import Vue from 'vue'
// 生产环境捕获错误日志
if (process.env.NODE_ENV === 'production') {
    Vue.config.errorHandler = (err, vm, info) => {
        // console.log(err, vm, info)
        if (!err) {
            return
        }

        let errStr = '\nerrorHandler 全局错误捕获:\n'
        errStr += `错误信息:\n name: ${err.name}\n message: ${
            err.message
        }\n stack: ${err.stack}\n`
        errStr += `vue组件tab名:${vm.$vnode.tag}\n`
        errStr += `报错钩子或位置信息:${info}\n`
        errStr += `报错日期:${new Date().toLocaleString()}\n`

        ipcRenderer.send('saveLog', errStr)
    }
}

4.4 HTTP 层面错误日志捕获

可以写在了 axios 拦截器中:

    // 请求拦截器
    axios.interceptors.request.use(
        (config) => {
        },
        (error) => {
            ...
            let errText = JSON.stringify(error)
            errText += `\ntype: requestError\ndate: ${new Date().toLocaleString()}`
            ipcRenderer.send('saveLog', `${errText}\n\n`)
            ...
        }
    )

   // 响应拦截器
    axios.interceptors.response.use(
        (response) => {
            if (response.status === 200 && response.data.code === '00000') {
                return response
            } else {
                ... 
                let errText = JSON.stringify(response)
                errText += `\ntype: responseError\ndate: ${new Date().toLocaleString()}`
                ipcRenderer.send('saveLog', `${errText}\n\n`)
                ...
            }
        },
        (error) => {
            if (error.response && error.response.status) {
                ...
                let errText = JSON.stringify(error.response)
                errText += `\ntype: responseError\ndate: ${new Date().toLocaleString()}`
                ipcRenderer.send('saveLog', `${errText}\n\n`)
                ...
            } else {
                ...
                let errText = JSON.stringify(error.response)
                errText += `\ntype: responseError\ndate: ${new Date().toLocaleString()}`
                ipcRenderer.send('saveLog', `${errText}\n\n`)
                ...
            }
            ...
        }
    )

4.5 其他错误日志捕获

在可能发生内存泄漏或者其他可疑的地方记录即可,注意信息尽可能详细,方便自己回溯分析问题。

4.6 调试利器:electron-log

npm地址:www.npmjs.com/package/ele…

通过electron-log这个包可以自动帮你监听崩溃信息,如render-process-gone事件触发的,但是要注意版本,因为5以上版本要 Electron 13+ 或Nodejs 14+。也能自定义记录一些信息,功能很强大,建议去官网了解详情。

转载自:https://juejin.cn/post/7331070714765836314
评论
请登录