mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2026-02-10 19:50:28 +08:00
fix: resolve event listener memory leaks and add error logging
This commit is contained in:
parent
ae91194a74
commit
b60c01bb4c
@ -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 {
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user