fix: resolve event listener memory leaks and add error logging

This commit is contained in:
xmk23333 2025-12-28 19:02:07 +08:00
parent ae91194a74
commit b60c01bb4c
4 changed files with 82 additions and 58 deletions

View File

@ -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<AxiosInstance> => {
const dynamicIpcPath = getMihomoIpcPath()
@ -233,6 +235,7 @@ export const mihomoSmartFlushCache = async (configName?: string): Promise<void>
}
export const startMihomoTraffic = async (): Promise<void> => {
trafficRetry = MAX_RETRY
await mihomoTraffic()
}
@ -258,7 +261,7 @@ const mihomoTraffic = async (): Promise<void> => {
mihomoTrafficWs.onmessage = async (e): Promise<void> => {
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<void> => {
}
export const startMihomoMemory = async (): Promise<void> => {
memoryRetry = MAX_RETRY
await mihomoMemory()
}
@ -314,7 +318,7 @@ const mihomoMemory = async (): Promise<void> => {
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<void> => {
}
export const startMihomoLogs = async (): Promise<void> => {
logsRetry = MAX_RETRY
await mihomoLogs()
}
@ -362,7 +367,7 @@ const mihomoLogs = async (): Promise<void> => {
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<void> => {
}
export const startMihomoConnections = async (): Promise<void> => {
connectionsRetry = MAX_RETRY
await mihomoConnections()
}
@ -408,7 +414,7 @@ const mihomoConnections = async (): Promise<void> => {
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 {

View File

@ -1,5 +1,6 @@
import { addProfileItem, getCurrentProfileItem, getProfileConfig } from '../config'
import { Cron } from 'croner'
import { logger } from '../utils/logger'
const intervalPool: Record<string, Cron | NodeJS.Timeout> = {}
@ -15,8 +16,8 @@ export async function initProfileUpdater(): Promise<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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)
}
})
}

View File

@ -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 (
<div className="app-drag h-screen w-screen overflow-hidden">
<div className="floating-bg border border-divider flex bg-content1 h-full w-full">

View File

@ -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> = (props) => {
})
const [series, setSeries] = useState(Array(10).fill(0))
// 使用 useRef 替代模块级变量
const currentUploadRef = useRef<number | undefined>(undefined)
const currentDownloadRef = useRef<number | undefined>(undefined)
const hasShowTrafficRef = useRef(false)
const drawingRef = useRef(false)
// Chart.js 配置
const chartData = useMemo(() => {
return {
@ -125,36 +126,45 @@ const ConnCard: React.FC<Props> = (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> = (props) => {
export default ConnCard
const drawSvg = async (upload: number, download: number): Promise<void> => {
if (upload === currentUpload && download === currentDownload) return
currentUpload = upload
currentDownload = download
const drawSvg = async (
upload: number,
download: number,
currentUploadRef: React.RefObject<number | undefined>,
currentDownloadRef: React.RefObject<number | undefined>
): Promise<void> => {
if (upload === currentUploadRef.current && download === currentDownloadRef.current) return
currentUploadRef.current = upload
currentDownloadRef.current = download
const svg = `data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 140 36"><image height="36" width="36" href="${trayIconBase64}"/><text x="140" y="15" font-size="18" font-family="PingFang SC" font-weight="bold" text-anchor="end">${calcTraffic(upload)}/s</text><text x="140" y="34" font-size="18" font-family="PingFang SC" font-weight="bold" text-anchor="end">${calcTraffic(download)}/s</text></svg>`
const image = await loadImage(svg)
window.electron.ipcRenderer.send('trayIconUpdate', image)