You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Electron应用内存泄漏警告排查与setMaxListeners()使用咨询

问题:Electron应用内存泄漏警告排查与解决

问题描述

开发的Electron应用每次运行时,控制台都会触发内存泄漏警告,不清楚该如何提升监听上限,想知道该错误是否与uiohook有关,以及如何使用emitter.setMaxListeners()解决问题。

控制台报错信息

[3452:1014/104426.056:ERROR:service_worker_storage.cc(1904)] Failed to delete the database: Database IO error
(node:21792) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 terminate-timer listeners added to [EventEmitter]. Use emitter.setMaxListeners() to increase limit
(Use `electron --trace-warnings ...` to show where the warning was created)
(node:21792) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 set-activity listeners added to [EventEmitter]. Use emitter.setMaxListeners() to increase limit

主进程代码

const electron = require('electron');
const BrowserWindow = electron.BrowserWindow;
const dialog = electron.dialog;
const desktopCapturer = electron.desktopCapturer;
const idle = electron.powerMonitor;
const remote = electron.remote;
const notification = electron.Notification;
const globalShortcut = electron.globalShortcut;
const screen = electron.screen;
const ipcMain = electron.ipcMain;
const app = electron.app;
const isDev = require('electron-is-dev');
const path = require('path');
const activeWindows = require('electron-active-window');
const activeWindowPage = require('active-win');
const BMParser = require('bookmark-parser');
const axios = require('axios');
const uiohook = require('uiohook-napi');
const { autoUpdater } = require("electron-updater");
const debug = require('debug')('Franz:ipcApi:autoUpdate');
const dns = require("dns");
const activity = { is_mouse: 0, is_keyboard: 0 };
const headers = {
    'Content-Type': 'application/json;charset=UTF-8',
    "Access-Control-Allow-Headers": "*",
    "Access-Control-Allow-Origin": "*",
    "Accept": "application/json"
}
const sessionUser = null;
let updateInterval = null;

app.disableHardwareAcceleration();
electron.powerSaveBlocker.start('prevent-app-suspension');
//electron.commandLine.appendSwitch ("disable-http-cache");

// Change App Name on Windows
if (process.platform === 'win32')
{
    app.setAppUserModelId('Rubii');
}

let apiEndpoint = "http://creaz:81/xorix/api"; // Brian
let frameUrl = 'http://localhost:3000'; // Brian

//if (isDev) { // or if(process.env.NODE_ENV)
//    apiEndpoint = "http://creaz:81/xorix/api";
//    frameUrl = 'http://localhost:3000';
//}

var internetAvailable = require("internet-available");

let isConnected = false;
let counterInternet = 0;
let allowSendInternetNotification = 1;

async function liveInternetCheck() {

    var userSessionCheck = await getSessionUser();

    // Most easy way
    internetAvailable({
        domainName: "rubii.com",
        // Provide maximum execution time for the verification
        timeout: 10000,
        // If it tries 10 times and it fails, then it will throw no internet
        retries: 10
    }).then(() => {
        // Available Internet
        isConnected = true;
        counterInternet++;
        if(counterInternet > 3 && allowSendInternetNotification == 0) {
            allowSendInternetNotification = 1;
        }
    }).catch(() => {
        // Not available internet
        isConnected = false;
        if(isConnected == false && userSessionCheck && allowSendInternetNotification == 1) {
            var message = 'Your internet has been disconnected! Please login again';
            new notification({ title: 'Ooops', body: message }).show();
            // Logout the user in the other end
            win.webContents.send("terminate-timer", "hello");
            allowSendInternetNotification = 0;
            counterInternet = 0;
        }
    });

}

autoUpdater.on("update-available", (_event, releaseNotes, releaseName) => {
    const dialogOpts = {
        type: 'info',
        buttons: ['Ok'],
        title: 'Update Available',
        message: process.platform === 'win32' ? releaseNotes : releaseName,
        detail: 'A new version download started. The app will be restarted to install the update.'
    };
    dialog.showMessageBox(dialogOpts);
    updateInterval = null;
});

autoUpdater.on("update-downloaded", (_event, releaseNotes, releaseName) => {
    const dialogOpts = {
        type: 'info',
        buttons: ['Restart'],
        title: 'Application Update',
        message: process.platform === 'win32' ? releaseNotes : releaseName,
        detail: 'A new version has been downloaded. Restart the application to apply the updates.'
    };
    dialog.showMessageBox(dialogOpts).then((returnValue) => {
        if (returnValue.response === 0) autoUpdater.quitAndInstall()
    });
});

function checkUpdate() {
    updateInterval = setInterval(() => autoUpdater.checkForUpdates(), 600000);
}

let win;

ipcMain.on('close-me', (evt, arg) => {
    new notification({ title: 'Ooops', body: 'We hope to see you again!' }).show();
    app.quit()
});

function determineScreenShotSize() {

    var screenSize = screen.getPrimaryDisplay().workAreaSize
    return {
        width: screenSize.width,
        height: screenSize.height
    }
}

async function takeScreenshot() {

    var user = await getSessionUser();
    var timer = await getSessionTimer();

    if (user && timer) {

        if (timer.timer == 1) {

            //console.log('Gathering screens...');

            //console.log(app.getPath('pictures'));
            const thumbSize = determineScreenShotSize();
            const workaroundTimestamp = Date.now();

            const options = {
                types: ['screen'],
                thumbnailSize: { ...thumbSize, workaroundTimestamp }
            };

            desktopCapturer.getSources(options).then((sources) => {
                console.log('Sources received:' + sources.length);

                sources.forEach(function (source) {
                    const sourceName = source.name.toLowerCase();
                    //console.log(sourceName);
                    if (['entire screen', 'screen 1'].includes(sourceName)) {

                        var the_screenshot = source.thumbnail.toPNG();

                        var data = {
                            user_id: user.id,
                            company_id: timer.company_id,
                            image: the_screenshot.toString('base64')
                        }

                        axios.post(apiEndpoint + "/desktop/save_screenshots", data, {
                            headers: headers
                        })
                        .then((response) => {
                            // console.log(response);
                        })
                        .catch((error) => {
                            //console.log(error);
                        })

                    }
                })
            }).catch(console.error);

        }
    }

}

function saveSoftware() {

    (async () => {
        var options = [];
        var software = await activeWindowPage();
        var user = await getSessionUser();
        var timer = await getSessionTimer();

        if (user && timer) {

            if (timer.timer == 1) {
                software.user = user;
                software.timer = timer;

                axios.post(apiEndpoint + "/desktop/save_app", software, {
                    headers: headers
                })
                .then((response) => {
                    //console.log(response);
                })
                .catch((error) => {
                    //console.log(error);
                })

            }
        }
    })();

}

function getSessionUser() {
    return win.webContents.executeJavaScript('sessionStorage.getItem("user");', true)
        .then(result => {
            if (result) {
                return JSON.parse(result);
            }
        });
}

function getSessionTimer() {
    return win.webContents.executeJavaScript('sessionStorage.getItem("timer");', true)
        .then(result => {
            if (result) {
                return JSON.parse(result);
            }
        });
}

async function generateActivity() {

    win.webContents.send("set-activity", activity);
    // We reset after we send it
    activity.is_mouse = activity.is_keyboard = 0;

}

function secondsToHms(d) {
    d = Number(d);
    var h = Math.floor(d / 3600);
    var m = Math.floor(d % 3600 / 60);
    var s = Math.floor(d % 3600 % 60);

    var hDisplay = h > 0 ? h + (h == 1 ? " hour, " : " hours, ") : "";
    var mDisplay = m > 0 ? m + (m == 1 ? " minute, " : " minutes, ") : "";
    var sDisplay = s > 0 ? s + (s == 1 ? " second" : " seconds") : "";
    return hDisplay + mDisplay + sDisplay;
}

function createWindow() {

    win = new BrowserWindow({
        width: 1200,
        height: 900,
        frame: true,
        webPreferences: {
            webSecurity: false,
            nodeIntegration: true,
            enableRemoteModule: true,
            contextIsolation: false,
            backgroundThrottling: false
        }
    });

    //win.loadURL('https://desktop.rubii.com');
    win.loadURL(frameUrl, { "extraHeaders": "pragma: no-cache\n" });

    // http://localhost:3000
    //win.webContents.openDevTools();

    //win.removeMenu(); // Brian
    // or set the Menu to null
    //win.setMenu(null); // Brian

    // Reload cache
    win.webContents.reloadIgnoringCache();

    // Keyboard activity
    uiohook.uIOhook.on('keydown', (e) => {
        //console.log('Keyboard!')
        activity.is_keyboard = 1;
    })

    // Mouse activity
    uiohook.uIOhook.on('mousemove', (e) => {
        //console.log('mouse');
        activity.is_mouse = 1;
    })

    // Start listener for keyboard and mouse
    uiohook.uIOhook.start();

    win.on('close', function () {
        win = null;
    })

}

app.whenReady().then(createWindow).then(checkUpdate);

app.on('window-all-closed', function () {
    app.quit();
});

app.on('activate', function () {
    if (win == null) {
        createWindow();
    }
})

setInterval(takeScreenshot, 10 * 60 * 1000); // 10 minutes
setInterval(saveSoftware, 5 * 1000); // 5 seconds
setInterval(generateActivity, 1 * 1000);
setInterval(function() { liveInternetCheck(); }, 10 * 1000); // 10 seconds

渲染进程代码

ipcRenderer.on("terminate-timer", function (event, data) {
  // Logout if there is no internet connection 
  handleLogout();
});

// Set activity if there were any keyboard, mouse
ipcRenderer.on("set-activity", function (event, data) {
  // If the timer is on 
  if(timer) {
    setActivity(data);
  }
});

问题分析与解决方案

1. 是否与uiohook有关?

无关。报错中提示的是terminate-timer和set-activity两个事件的监听器数量超限,这两个是主进程通过win.webContents.send发送、渲染进程用ipcRenderer.on监听的IPC事件,和uiohook监听的keydown、mousemove事件没有关联。

2. 监听器超限的原因

代码中app.on('activate')会在窗口关闭后重新调用createWindow创建新窗口,而渲染进程每次加载时都会执行ipcRenderer.on添加新的监听器,但旧窗口关闭时没有移除这些监听器,导致监听器不断累积,超过了EventEmitter默认的10个上限。

3. 解决方法

方法一:移除旧监听器(推荐,从根源解决)

在渲染进程中,将监听器函数单独定义,方便在窗口关闭时移除:

// 渲染进程修改后代码
// 单独定义监听器函数
function handleTerminateTimer(event, data) {
  // Logout if there is no internet connection 
  handleLogout();
}

function handleSetActivity(event, data) {
  // If the timer is on 
  if(timer) {
    setActivity(data);
  }
}

// 添加监听器
ipcRenderer.on("terminate-timer", handleTerminateTimer);
ipcRenderer.on("set-activity", handleSetActivity);

// 窗口关闭前移除监听器
window.addEventListener('beforeunload', () => {
  ipcRenderer.removeListener("terminate-timer", handleTerminateTimer);
  ipcRenderer.removeListener("set-activity", handleSetActivity);
});

方法二:提高监听器上限(临时解决)

如果确定需要保留多个监听器,可以修改EventEmitter的默认上限,或单独给ipcRenderer设置上限:

  • 全局设置(主进程或渲染进程开头):
// 提高到20个,可根据需求调整
require('events').EventEmitter.defaultMaxListeners = 20;
  • 单独给ipcRenderer设置(渲染进程):
ipcRenderer.setMaxListeners(20);

4. 额外优化:清理全局定时器

主进程中的setInterval是全局的,窗口关闭后不会自动清除,会导致内存泄漏。需要在窗口关闭时手动清除:

// 主进程修改:将setInterval赋值给变量
const takeScreenshotInterval = setInterval(takeScreenshot, 10 * 60 * 1000); // 10 minutes
const saveSoftwareInterval = setInterval(saveSoftware, 5 * 1000); // 5 seconds
const generateActivityInterval = setInterval(generateActivity, 1 * 1000);
const liveInternetCheckInterval = setInterval(liveInternetCheck, 10 * 1000); // 10 seconds

// 修改createWindow中的win.close事件
win.on('close', function () {
  // 清除所有全局定时器
  clearInterval(updateInterval);
  clearInterval(takeScreenshotInterval);
  clearInterval(saveSoftwareInterval);
  clearInterval(generateActivityInterval);
  clearInterval(liveInternetCheckInterval);
  win = null;
});

内容的提问来源于stack exchange,提问作者Brian Millot

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 18:31:16