mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-17 11:07:11 +08:00
fix(desktop): prevent backend zombie process and IPC listener leaks
This commit is contained in:
@@ -7,12 +7,18 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
selectDirectory: () => ipcRenderer.invoke('select-directory'),
|
selectDirectory: () => ipcRenderer.invoke('select-directory'),
|
||||||
selectFile: (filters?: Electron.FileFilter[]) => ipcRenderer.invoke('select-file', filters),
|
selectFile: (filters?: Electron.FileFilter[]) => ipcRenderer.invoke('select-file', filters),
|
||||||
|
|
||||||
|
// Each listener registrar returns an unsubscribe fn so renderers can clean
|
||||||
|
// up on unmount / effect re-run and avoid accumulating duplicate handlers.
|
||||||
onBackendStatus: (callback: (data: { status: string; port?: number; error?: string }) => void) => {
|
onBackendStatus: (callback: (data: { status: string; port?: number; error?: string }) => void) => {
|
||||||
ipcRenderer.on('backend-status', (_event, data) => callback(data))
|
const handler = (_event: unknown, data: { status: string; port?: number; error?: string }) => callback(data)
|
||||||
|
ipcRenderer.on('backend-status', handler)
|
||||||
|
return () => ipcRenderer.removeListener('backend-status', handler)
|
||||||
},
|
},
|
||||||
|
|
||||||
onBackendLog: (callback: (line: string) => void) => {
|
onBackendLog: (callback: (line: string) => void) => {
|
||||||
ipcRenderer.on('backend-log', (_event, line) => callback(line))
|
const handler = (_event: unknown, line: string) => callback(line)
|
||||||
|
ipcRenderer.on('backend-log', handler)
|
||||||
|
return () => ipcRenderer.removeListener('backend-log', handler)
|
||||||
},
|
},
|
||||||
|
|
||||||
// Window controls (custom titlebar on Windows)
|
// Window controls (custom titlebar on Windows)
|
||||||
@@ -21,12 +27,16 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
windowClose: () => ipcRenderer.invoke('window-close'),
|
windowClose: () => ipcRenderer.invoke('window-close'),
|
||||||
windowIsMaximized: () => ipcRenderer.invoke('window-is-maximized'),
|
windowIsMaximized: () => ipcRenderer.invoke('window-is-maximized'),
|
||||||
onMaximizeChange: (callback: (maximized: boolean) => void) => {
|
onMaximizeChange: (callback: (maximized: boolean) => void) => {
|
||||||
ipcRenderer.on('window-maximize-changed', (_event, max) => callback(max))
|
const handler = (_event: unknown, max: boolean) => callback(max)
|
||||||
|
ipcRenderer.on('window-maximize-changed', handler)
|
||||||
|
return () => ipcRenderer.removeListener('window-maximize-changed', handler)
|
||||||
},
|
},
|
||||||
|
|
||||||
// App menu / shortcut actions forwarded from the main process.
|
// App menu / shortcut actions forwarded from the main process.
|
||||||
onMenuAction: (callback: (action: string) => void) => {
|
onMenuAction: (callback: (action: string) => void) => {
|
||||||
ipcRenderer.on('menu-action', (_event, action: string) => callback(action))
|
const handler = (_event: unknown, action: string) => callback(action)
|
||||||
|
ipcRenderer.on('menu-action', handler)
|
||||||
|
return () => ipcRenderer.removeListener('menu-action', handler)
|
||||||
},
|
},
|
||||||
|
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
|
|||||||
@@ -176,11 +176,15 @@ export class PythonBackend extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stop(): void {
|
stop(): void {
|
||||||
if (this.process) {
|
const proc = this.process
|
||||||
this.process.kill('SIGTERM')
|
if (proc) {
|
||||||
|
proc.kill('SIGTERM')
|
||||||
|
// Keep a local ref so the SIGKILL fallback can still reach the process
|
||||||
|
// even after we clear `this.process`; otherwise a stuck backend would
|
||||||
|
// never be force-killed and leak as a zombie.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (this.process && !this.process.killed) {
|
if (!proc.killed) {
|
||||||
this.process.kill('SIGKILL')
|
proc.kill('SIGKILL')
|
||||||
}
|
}
|
||||||
}, 5000)
|
}, 5000)
|
||||||
this.process = null
|
this.process = null
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const App: React.FC = () => {
|
|||||||
|
|
||||||
// Handle app-menu / shortcut actions forwarded from the main process.
|
// Handle app-menu / shortcut actions forwarded from the main process.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.electronAPI?.onMenuAction?.((action) => {
|
const off = window.electronAPI?.onMenuAction?.((action) => {
|
||||||
if (action === 'new-chat') {
|
if (action === 'new-chat') {
|
||||||
useSessionStore.getState().newSession()
|
useSessionStore.getState().newSession()
|
||||||
navigate('/')
|
navigate('/')
|
||||||
@@ -42,6 +42,7 @@ const App: React.FC = () => {
|
|||||||
navigate('/settings')
|
navigate('/settings')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
return off
|
||||||
}, [navigate])
|
}, [navigate])
|
||||||
|
|
||||||
const handleLangChange = useCallback(() => forceUpdate((n) => n + 1), [])
|
const handleLangChange = useCallback(() => forceUpdate((n) => n + 1), [])
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export function useBackend() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
|
let offStatus: (() => void) | undefined
|
||||||
const api = window.electronAPI
|
const api = window.electronAPI
|
||||||
|
|
||||||
const startPolling = async (port: number) => {
|
const startPolling = async (port: number) => {
|
||||||
@@ -62,7 +63,7 @@ export function useBackend() {
|
|||||||
startPolling(p)
|
startPolling(p)
|
||||||
})
|
})
|
||||||
|
|
||||||
api.onBackendStatus((data) => {
|
offStatus = api.onBackendStatus((data) => {
|
||||||
if (data.status === 'ready' && data.port) {
|
if (data.status === 'ready' && data.port) {
|
||||||
setState({ status: 'ready', port: data.port })
|
setState({ status: 'ready', port: data.port })
|
||||||
if (pollingRef.current) {
|
if (pollingRef.current) {
|
||||||
@@ -82,6 +83,7 @@ export function useBackend() {
|
|||||||
if (pollingRef.current) {
|
if (pollingRef.current) {
|
||||||
clearTimeout(pollingRef.current)
|
clearTimeout(pollingRef.current)
|
||||||
}
|
}
|
||||||
|
offStatus?.()
|
||||||
}
|
}
|
||||||
}, [probeBackend])
|
}, [probeBackend])
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ const WindowControls: React.FC = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api?.windowIsMaximized().then(setMaximized)
|
api?.windowIsMaximized().then(setMaximized)
|
||||||
api?.onMaximizeChange(setMaximized)
|
const off = api?.onMaximizeChange(setMaximized)
|
||||||
|
return off
|
||||||
}, [api])
|
}, [api])
|
||||||
|
|
||||||
if (api?.platform === 'darwin') return null
|
if (api?.platform === 'darwin') return null
|
||||||
|
|||||||
@@ -8,14 +8,15 @@ export interface ElectronAPI {
|
|||||||
restartBackend: () => Promise<boolean>
|
restartBackend: () => Promise<boolean>
|
||||||
selectDirectory: () => Promise<string | null>
|
selectDirectory: () => Promise<string | null>
|
||||||
selectFile: (filters?: { name: string; extensions: string[] }[]) => Promise<string | null>
|
selectFile: (filters?: { name: string; extensions: string[] }[]) => Promise<string | null>
|
||||||
onBackendStatus: (callback: (data: BackendStatusEvent) => void) => void
|
// Listener registrars return an unsubscribe fn for cleanup.
|
||||||
onBackendLog: (callback: (line: string) => void) => void
|
onBackendStatus: (callback: (data: BackendStatusEvent) => void) => () => void
|
||||||
|
onBackendLog: (callback: (line: string) => void) => () => void
|
||||||
windowMinimize: () => Promise<void>
|
windowMinimize: () => Promise<void>
|
||||||
windowMaximize: () => Promise<boolean>
|
windowMaximize: () => Promise<boolean>
|
||||||
windowClose: () => Promise<void>
|
windowClose: () => Promise<void>
|
||||||
windowIsMaximized: () => Promise<boolean>
|
windowIsMaximized: () => Promise<boolean>
|
||||||
onMaximizeChange: (callback: (maximized: boolean) => void) => void
|
onMaximizeChange: (callback: (maximized: boolean) => void) => () => void
|
||||||
onMenuAction?: (callback: (action: string) => void) => void
|
onMenuAction?: (callback: (action: string) => void) => () => void
|
||||||
platform: string
|
platform: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user