From b60c01bb4ca68712b8d888c868b8bf4eb64cc186 Mon Sep 17 00:00:00 2001 From: xmk23333 Date: Sun, 28 Dec 2025 19:02:07 +0800 Subject: [PATCH] fix: resolve event listener memory leaks and add error logging --- src/main/core/mihomoApi.ts | 14 ++-- src/main/core/profileUpdater.ts | 37 +++++----- src/renderer/src/FloatingApp.tsx | 22 +++--- .../src/components/sider/conn-card.tsx | 67 ++++++++++++------- 4 files changed, 82 insertions(+), 58 deletions(-) diff --git a/src/main/core/mihomoApi.ts b/src/main/core/mihomoApi.ts index fbc2838..2f9ae0d 100644 --- a/src/main/core/mihomoApi.ts +++ b/src/main/core/mihomoApi.ts @@ -19,6 +19,8 @@ let logsRetry = 10 let mihomoConnectionsWs: WebSocket | null = null let connectionsRetry = 10 +const MAX_RETRY = 10 + export const getAxios = async (force: boolean = false): Promise => { const dynamicIpcPath = getMihomoIpcPath() @@ -233,6 +235,7 @@ export const mihomoSmartFlushCache = async (configName?: string): Promise } export const startMihomoTraffic = async (): Promise => { + trafficRetry = MAX_RETRY await mihomoTraffic() } @@ -258,7 +261,7 @@ const mihomoTraffic = async (): Promise => { mihomoTrafficWs.onmessage = async (e): Promise => { const data = e.data as string const json = JSON.parse(data) as IMihomoTrafficInfo - trafficRetry = 10 + trafficRetry = MAX_RETRY try { mainWindow?.webContents.send('mihomoTraffic', json) if (process.platform !== 'linux') { @@ -292,6 +295,7 @@ const mihomoTraffic = async (): Promise => { } export const startMihomoMemory = async (): Promise => { + memoryRetry = MAX_RETRY await mihomoMemory() } @@ -314,7 +318,7 @@ const mihomoMemory = async (): Promise => { mihomoMemoryWs.onmessage = (e): void => { const data = e.data as string - memoryRetry = 10 + memoryRetry = MAX_RETRY try { mainWindow?.webContents.send('mihomoMemory', JSON.parse(data) as IMihomoMemoryInfo) } catch { @@ -338,6 +342,7 @@ const mihomoMemory = async (): Promise => { } export const startMihomoLogs = async (): Promise => { + logsRetry = MAX_RETRY await mihomoLogs() } @@ -362,7 +367,7 @@ const mihomoLogs = async (): Promise => { mihomoLogsWs.onmessage = (e): void => { const data = e.data as string - logsRetry = 10 + logsRetry = MAX_RETRY try { mainWindow?.webContents.send('mihomoLogs', JSON.parse(data) as IMihomoLogInfo) } catch { @@ -386,6 +391,7 @@ const mihomoLogs = async (): Promise => { } export const startMihomoConnections = async (): Promise => { + connectionsRetry = MAX_RETRY await mihomoConnections() } @@ -408,7 +414,7 @@ const mihomoConnections = async (): Promise => { mihomoConnectionsWs.onmessage = (e): void => { const data = e.data as string - connectionsRetry = 10 + connectionsRetry = MAX_RETRY try { mainWindow?.webContents.send('mihomoConnections', JSON.parse(data) as IMihomoConnectionsInfo) } catch { diff --git a/src/main/core/profileUpdater.ts b/src/main/core/profileUpdater.ts index 56fb167..208cf76 100644 --- a/src/main/core/profileUpdater.ts +++ b/src/main/core/profileUpdater.ts @@ -1,5 +1,6 @@ import { addProfileItem, getCurrentProfileItem, getProfileConfig } from '../config' import { Cron } from 'croner' +import { logger } from '../utils/logger' const intervalPool: Record = {} @@ -15,8 +16,8 @@ export async function initProfileUpdater(): Promise { async () => { try { await addProfileItem(item) - } catch { - /* ignore */ + } catch (e) { + await logger.warn(`[ProfileUpdater] Failed to update profile ${item.name}:`, e) } }, item.interval * 60 * 1000 @@ -26,16 +27,16 @@ export async function initProfileUpdater(): Promise { intervalPool[item.id] = new Cron(item.interval, async () => { try { await addProfileItem(item) - } catch { - /* ignore */ + } catch (e) { + await logger.warn(`[ProfileUpdater] Failed to update profile ${item.name}:`, e) } }) } try { await addProfileItem(item) - } catch { - /* ignore */ + } catch (e) { + await logger.warn(`[ProfileUpdater] Failed to init profile ${item.name}:`, e) } } } @@ -46,8 +47,8 @@ export async function initProfileUpdater(): Promise { async () => { try { await addProfileItem(currentItem) - } catch { - /* ignore */ + } catch (e) { + await logger.warn(`[ProfileUpdater] Failed to update current profile:`, e) } }, currentItem.interval * 60 * 1000 @@ -57,8 +58,8 @@ export async function initProfileUpdater(): Promise { async () => { try { await addProfileItem(currentItem) - } catch { - /* ignore */ + } catch (e) { + await logger.warn(`[ProfileUpdater] Failed to update current profile:`, e) } }, currentItem.interval * 60 * 1000 + 10000 // +10s @@ -67,16 +68,16 @@ export async function initProfileUpdater(): Promise { intervalPool[currentItem.id] = new Cron(currentItem.interval, async () => { try { await addProfileItem(currentItem) - } catch { - /* ignore */ + } catch (e) { + await logger.warn(`[ProfileUpdater] Failed to update current profile:`, e) } }) } try { await addProfileItem(currentItem) - } catch { - /* ignore */ + } catch (e) { + await logger.warn(`[ProfileUpdater] Failed to init current profile:`, e) } } } @@ -96,8 +97,8 @@ export async function addProfileUpdater(item: IProfileItem): Promise { async () => { try { await addProfileItem(item) - } catch { - /* ignore */ + } catch (e) { + await logger.warn(`[ProfileUpdater] Failed to update profile ${item.name}:`, e) } }, item.interval * 60 * 1000 @@ -106,8 +107,8 @@ export async function addProfileUpdater(item: IProfileItem): Promise { intervalPool[item.id] = new Cron(item.interval, async () => { try { await addProfileItem(item) - } catch { - /* ignore */ + } catch (e) { + await logger.warn(`[ProfileUpdater] Failed to update profile ${item.name}:`, e) } }) } diff --git a/src/renderer/src/FloatingApp.tsx b/src/renderer/src/FloatingApp.tsx index 64fc4b3..efe5128 100644 --- a/src/renderer/src/FloatingApp.tsx +++ b/src/renderer/src/FloatingApp.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react' +import { useEffect, useMemo, useState, useCallback } from 'react' import MihomoIcon from './components/base/mihomo-icon' import { calcTraffic } from './utils/calc' import { showContextMenu, triggerMainWindow } from './utils/ipc' @@ -48,17 +48,19 @@ const FloatingApp: React.FC = () => { } }, [spinSpeed, spinFloatingIcon]) - useEffect(() => { - window.electron.ipcRenderer.on('mihomoTraffic', async (_e, ...args) => { - const info = args[0] as IMihomoTrafficInfo - setUpload(info.up) - setDownload(info.down) - }) - return (): void => { - window.electron.ipcRenderer.removeAllListeners('mihomoTraffic') - } + const handleTraffic = useCallback((_e: unknown, ...args: unknown[]) => { + const info = args[0] as IMihomoTrafficInfo + setUpload(info.up) + setDownload(info.down) }, []) + useEffect(() => { + window.electron.ipcRenderer.on('mihomoTraffic', handleTraffic) + return (): void => { + window.electron.ipcRenderer.removeListener('mihomoTraffic', handleTraffic) + } + }, [handleTraffic]) + return (
diff --git a/src/renderer/src/components/sider/conn-card.tsx b/src/renderer/src/components/sider/conn-card.tsx index 29f2e4b..118f564 100644 --- a/src/renderer/src/components/sider/conn-card.tsx +++ b/src/renderer/src/components/sider/conn-card.tsx @@ -2,7 +2,7 @@ import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react' import { FaCircleArrowDown, FaCircleArrowUp } from 'react-icons/fa6' import { useLocation, useNavigate } from 'react-router-dom' import { calcTraffic } from '@renderer/utils/calc' -import React, { useEffect, useState, useMemo } from 'react' +import React, { useEffect, useState, useMemo, useRef, useCallback } from 'react' import { useSortable } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import { IoLink } from 'react-icons/io5' @@ -25,11 +25,6 @@ import { useTranslation } from 'react-i18next' // 注册 Chart.js 组件 ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Filler) -let currentUpload: number | undefined = undefined -let currentDownload: number | undefined = undefined -let hasShowTraffic = false -let drawing = false - interface Props { iconOnly?: boolean } @@ -61,6 +56,12 @@ const ConnCard: React.FC = (props) => { }) const [series, setSeries] = useState(Array(10).fill(0)) + // 使用 useRef 替代模块级变量 + const currentUploadRef = useRef(undefined) + const currentDownloadRef = useRef(undefined) + const hasShowTrafficRef = useRef(false) + const drawingRef = useRef(false) + // Chart.js 配置 const chartData = useMemo(() => { return { @@ -125,36 +126,45 @@ const ConnCard: React.FC = (props) => { } const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null - useEffect(() => { - window.electron.ipcRenderer.on('mihomoTraffic', async (_e, ...args) => { + + // 使用 useCallback 创建稳定的 handler 引用 + const handleTraffic = useCallback( + async (_e: unknown, ...args: unknown[]) => { const info = args[0] as IMihomoTrafficInfo setUpload(info.up) setDownload(info.down) - const data = series - data.shift() - data.push(info.up + info.down) - setSeries([...data]) + setSeries((prev) => { + const data = [...prev] + data.shift() + data.push(info.up + info.down) + return data + }) if (platform === 'darwin' && showTraffic) { - if (drawing) return - drawing = true + if (drawingRef.current) return + drawingRef.current = true try { - await drawSvg(info.up, info.down) - hasShowTraffic = true + await drawSvg(info.up, info.down, currentUploadRef, currentDownloadRef) + hasShowTrafficRef.current = true } catch { // ignore } finally { - drawing = false + drawingRef.current = false } } else { - if (!hasShowTraffic) return + if (!hasShowTrafficRef.current) return window.electron.ipcRenderer.send('trayIconUpdate', trayIconBase64) - hasShowTraffic = false + hasShowTrafficRef.current = false } - }) + }, + [showTraffic] + ) + + useEffect(() => { + window.electron.ipcRenderer.on('mihomoTraffic', handleTraffic) return (): void => { - window.electron.ipcRenderer.removeAllListeners('mihomoTraffic') + window.electron.ipcRenderer.removeListener('mihomoTraffic', handleTraffic) } - }, [showTraffic]) + }, [handleTraffic]) if (iconOnly) { return ( @@ -274,10 +284,15 @@ const ConnCard: React.FC = (props) => { export default ConnCard -const drawSvg = async (upload: number, download: number): Promise => { - if (upload === currentUpload && download === currentDownload) return - currentUpload = upload - currentDownload = download +const drawSvg = async ( + upload: number, + download: number, + currentUploadRef: React.RefObject, + currentDownloadRef: React.RefObject +): Promise => { + if (upload === currentUploadRef.current && download === currentDownloadRef.current) return + currentUploadRef.current = upload + currentDownloadRef.current = download const svg = `data:image/svg+xml;utf8,${calcTraffic(upload)}/s${calcTraffic(download)}/s` const image = await loadImage(svg) window.electron.ipcRenderer.send('trayIconUpdate', image)