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 mihomoConnectionsWs: WebSocket | null = null
let connectionsRetry = 10 let connectionsRetry = 10
const MAX_RETRY = 10
export const getAxios = async (force: boolean = false): Promise<AxiosInstance> => { export const getAxios = async (force: boolean = false): Promise<AxiosInstance> => {
const dynamicIpcPath = getMihomoIpcPath() const dynamicIpcPath = getMihomoIpcPath()
@ -233,6 +235,7 @@ export const mihomoSmartFlushCache = async (configName?: string): Promise<void>
} }
export const startMihomoTraffic = async (): Promise<void> => { export const startMihomoTraffic = async (): Promise<void> => {
trafficRetry = MAX_RETRY
await mihomoTraffic() await mihomoTraffic()
} }
@ -258,7 +261,7 @@ const mihomoTraffic = async (): Promise<void> => {
mihomoTrafficWs.onmessage = async (e): Promise<void> => { mihomoTrafficWs.onmessage = async (e): Promise<void> => {
const data = e.data as string const data = e.data as string
const json = JSON.parse(data) as IMihomoTrafficInfo const json = JSON.parse(data) as IMihomoTrafficInfo
trafficRetry = 10 trafficRetry = MAX_RETRY
try { try {
mainWindow?.webContents.send('mihomoTraffic', json) mainWindow?.webContents.send('mihomoTraffic', json)
if (process.platform !== 'linux') { if (process.platform !== 'linux') {
@ -292,6 +295,7 @@ const mihomoTraffic = async (): Promise<void> => {
} }
export const startMihomoMemory = async (): Promise<void> => { export const startMihomoMemory = async (): Promise<void> => {
memoryRetry = MAX_RETRY
await mihomoMemory() await mihomoMemory()
} }
@ -314,7 +318,7 @@ const mihomoMemory = async (): Promise<void> => {
mihomoMemoryWs.onmessage = (e): void => { mihomoMemoryWs.onmessage = (e): void => {
const data = e.data as string const data = e.data as string
memoryRetry = 10 memoryRetry = MAX_RETRY
try { try {
mainWindow?.webContents.send('mihomoMemory', JSON.parse(data) as IMihomoMemoryInfo) mainWindow?.webContents.send('mihomoMemory', JSON.parse(data) as IMihomoMemoryInfo)
} catch { } catch {
@ -338,6 +342,7 @@ const mihomoMemory = async (): Promise<void> => {
} }
export const startMihomoLogs = async (): Promise<void> => { export const startMihomoLogs = async (): Promise<void> => {
logsRetry = MAX_RETRY
await mihomoLogs() await mihomoLogs()
} }
@ -362,7 +367,7 @@ const mihomoLogs = async (): Promise<void> => {
mihomoLogsWs.onmessage = (e): void => { mihomoLogsWs.onmessage = (e): void => {
const data = e.data as string const data = e.data as string
logsRetry = 10 logsRetry = MAX_RETRY
try { try {
mainWindow?.webContents.send('mihomoLogs', JSON.parse(data) as IMihomoLogInfo) mainWindow?.webContents.send('mihomoLogs', JSON.parse(data) as IMihomoLogInfo)
} catch { } catch {
@ -386,6 +391,7 @@ const mihomoLogs = async (): Promise<void> => {
} }
export const startMihomoConnections = async (): Promise<void> => { export const startMihomoConnections = async (): Promise<void> => {
connectionsRetry = MAX_RETRY
await mihomoConnections() await mihomoConnections()
} }
@ -408,7 +414,7 @@ const mihomoConnections = async (): Promise<void> => {
mihomoConnectionsWs.onmessage = (e): void => { mihomoConnectionsWs.onmessage = (e): void => {
const data = e.data as string const data = e.data as string
connectionsRetry = 10 connectionsRetry = MAX_RETRY
try { try {
mainWindow?.webContents.send('mihomoConnections', JSON.parse(data) as IMihomoConnectionsInfo) mainWindow?.webContents.send('mihomoConnections', JSON.parse(data) as IMihomoConnectionsInfo)
} catch { } catch {

View File

@ -1,5 +1,6 @@
import { addProfileItem, getCurrentProfileItem, getProfileConfig } from '../config' import { addProfileItem, getCurrentProfileItem, getProfileConfig } from '../config'
import { Cron } from 'croner' import { Cron } from 'croner'
import { logger } from '../utils/logger'
const intervalPool: Record<string, Cron | NodeJS.Timeout> = {} const intervalPool: Record<string, Cron | NodeJS.Timeout> = {}
@ -15,8 +16,8 @@ export async function initProfileUpdater(): Promise<void> {
async () => { async () => {
try { try {
await addProfileItem(item) await addProfileItem(item)
} catch { } catch (e) {
/* ignore */ await logger.warn(`[ProfileUpdater] Failed to update profile ${item.name}:`, e)
} }
}, },
item.interval * 60 * 1000 item.interval * 60 * 1000
@ -26,16 +27,16 @@ export async function initProfileUpdater(): Promise<void> {
intervalPool[item.id] = new Cron(item.interval, async () => { intervalPool[item.id] = new Cron(item.interval, async () => {
try { try {
await addProfileItem(item) await addProfileItem(item)
} catch { } catch (e) {
/* ignore */ await logger.warn(`[ProfileUpdater] Failed to update profile ${item.name}:`, e)
} }
}) })
} }
try { try {
await addProfileItem(item) await addProfileItem(item)
} catch { } catch (e) {
/* ignore */ await logger.warn(`[ProfileUpdater] Failed to init profile ${item.name}:`, e)
} }
} }
} }
@ -46,8 +47,8 @@ export async function initProfileUpdater(): Promise<void> {
async () => { async () => {
try { try {
await addProfileItem(currentItem) await addProfileItem(currentItem)
} catch { } catch (e) {
/* ignore */ await logger.warn(`[ProfileUpdater] Failed to update current profile:`, e)
} }
}, },
currentItem.interval * 60 * 1000 currentItem.interval * 60 * 1000
@ -57,8 +58,8 @@ export async function initProfileUpdater(): Promise<void> {
async () => { async () => {
try { try {
await addProfileItem(currentItem) await addProfileItem(currentItem)
} catch { } catch (e) {
/* ignore */ await logger.warn(`[ProfileUpdater] Failed to update current profile:`, e)
} }
}, },
currentItem.interval * 60 * 1000 + 10000 // +10s currentItem.interval * 60 * 1000 + 10000 // +10s
@ -67,16 +68,16 @@ export async function initProfileUpdater(): Promise<void> {
intervalPool[currentItem.id] = new Cron(currentItem.interval, async () => { intervalPool[currentItem.id] = new Cron(currentItem.interval, async () => {
try { try {
await addProfileItem(currentItem) await addProfileItem(currentItem)
} catch { } catch (e) {
/* ignore */ await logger.warn(`[ProfileUpdater] Failed to update current profile:`, e)
} }
}) })
} }
try { try {
await addProfileItem(currentItem) await addProfileItem(currentItem)
} catch { } catch (e) {
/* ignore */ await logger.warn(`[ProfileUpdater] Failed to init current profile:`, e)
} }
} }
} }
@ -96,8 +97,8 @@ export async function addProfileUpdater(item: IProfileItem): Promise<void> {
async () => { async () => {
try { try {
await addProfileItem(item) await addProfileItem(item)
} catch { } catch (e) {
/* ignore */ await logger.warn(`[ProfileUpdater] Failed to update profile ${item.name}:`, e)
} }
}, },
item.interval * 60 * 1000 item.interval * 60 * 1000
@ -106,8 +107,8 @@ export async function addProfileUpdater(item: IProfileItem): Promise<void> {
intervalPool[item.id] = new Cron(item.interval, async () => { intervalPool[item.id] = new Cron(item.interval, async () => {
try { try {
await addProfileItem(item) await addProfileItem(item)
} catch { } catch (e) {
/* ignore */ 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 MihomoIcon from './components/base/mihomo-icon'
import { calcTraffic } from './utils/calc' import { calcTraffic } from './utils/calc'
import { showContextMenu, triggerMainWindow } from './utils/ipc' import { showContextMenu, triggerMainWindow } from './utils/ipc'
@ -48,17 +48,19 @@ const FloatingApp: React.FC = () => {
} }
}, [spinSpeed, spinFloatingIcon]) }, [spinSpeed, spinFloatingIcon])
useEffect(() => { const handleTraffic = useCallback((_e: unknown, ...args: unknown[]) => {
window.electron.ipcRenderer.on('mihomoTraffic', async (_e, ...args) => { const info = args[0] as IMihomoTrafficInfo
const info = args[0] as IMihomoTrafficInfo setUpload(info.up)
setUpload(info.up) setDownload(info.down)
setDownload(info.down)
})
return (): void => {
window.electron.ipcRenderer.removeAllListeners('mihomoTraffic')
}
}, []) }, [])
useEffect(() => {
window.electron.ipcRenderer.on('mihomoTraffic', handleTraffic)
return (): void => {
window.electron.ipcRenderer.removeListener('mihomoTraffic', handleTraffic)
}
}, [handleTraffic])
return ( return (
<div className="app-drag h-screen w-screen overflow-hidden"> <div className="app-drag h-screen w-screen overflow-hidden">
<div className="floating-bg border border-divider flex bg-content1 h-full w-full"> <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 { FaCircleArrowDown, FaCircleArrowUp } from 'react-icons/fa6'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { calcTraffic } from '@renderer/utils/calc' 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 { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities' import { CSS } from '@dnd-kit/utilities'
import { IoLink } from 'react-icons/io5' import { IoLink } from 'react-icons/io5'
@ -25,11 +25,6 @@ import { useTranslation } from 'react-i18next'
// 注册 Chart.js 组件 // 注册 Chart.js 组件
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Filler) 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 { interface Props {
iconOnly?: boolean iconOnly?: boolean
} }
@ -61,6 +56,12 @@ const ConnCard: React.FC<Props> = (props) => {
}) })
const [series, setSeries] = useState(Array(10).fill(0)) 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 配置 // Chart.js 配置
const chartData = useMemo(() => { const chartData = useMemo(() => {
return { 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 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 const info = args[0] as IMihomoTrafficInfo
setUpload(info.up) setUpload(info.up)
setDownload(info.down) setDownload(info.down)
const data = series setSeries((prev) => {
data.shift() const data = [...prev]
data.push(info.up + info.down) data.shift()
setSeries([...data]) data.push(info.up + info.down)
return data
})
if (platform === 'darwin' && showTraffic) { if (platform === 'darwin' && showTraffic) {
if (drawing) return if (drawingRef.current) return
drawing = true drawingRef.current = true
try { try {
await drawSvg(info.up, info.down) await drawSvg(info.up, info.down, currentUploadRef, currentDownloadRef)
hasShowTraffic = true hasShowTrafficRef.current = true
} catch { } catch {
// ignore // ignore
} finally { } finally {
drawing = false drawingRef.current = false
} }
} else { } else {
if (!hasShowTraffic) return if (!hasShowTrafficRef.current) return
window.electron.ipcRenderer.send('trayIconUpdate', trayIconBase64) window.electron.ipcRenderer.send('trayIconUpdate', trayIconBase64)
hasShowTraffic = false hasShowTrafficRef.current = false
} }
}) },
[showTraffic]
)
useEffect(() => {
window.electron.ipcRenderer.on('mihomoTraffic', handleTraffic)
return (): void => { return (): void => {
window.electron.ipcRenderer.removeAllListeners('mihomoTraffic') window.electron.ipcRenderer.removeListener('mihomoTraffic', handleTraffic)
} }
}, [showTraffic]) }, [handleTraffic])
if (iconOnly) { if (iconOnly) {
return ( return (
@ -274,10 +284,15 @@ const ConnCard: React.FC<Props> = (props) => {
export default ConnCard export default ConnCard
const drawSvg = async (upload: number, download: number): Promise<void> => { const drawSvg = async (
if (upload === currentUpload && download === currentDownload) return upload: number,
currentUpload = upload download: number,
currentDownload = download 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 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) const image = await loadImage(svg)
window.electron.ipcRenderer.send('trayIconUpdate', image) window.electron.ipcRenderer.send('trayIconUpdate', image)