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'),
|
||||
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) => {
|
||||
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) => {
|
||||
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)
|
||||
@@ -21,12 +27,16 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
windowClose: () => ipcRenderer.invoke('window-close'),
|
||||
windowIsMaximized: () => ipcRenderer.invoke('window-is-maximized'),
|
||||
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.
|
||||
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,
|
||||
|
||||
@@ -176,11 +176,15 @@ export class PythonBackend extends EventEmitter {
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
if (this.process) {
|
||||
this.process.kill('SIGTERM')
|
||||
const proc = this.process
|
||||
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(() => {
|
||||
if (this.process && !this.process.killed) {
|
||||
this.process.kill('SIGKILL')
|
||||
if (!proc.killed) {
|
||||
proc.kill('SIGKILL')
|
||||
}
|
||||
}, 5000)
|
||||
this.process = null
|
||||
|
||||
@@ -34,7 +34,7 @@ const App: React.FC = () => {
|
||||
|
||||
// Handle app-menu / shortcut actions forwarded from the main process.
|
||||
useEffect(() => {
|
||||
window.electronAPI?.onMenuAction?.((action) => {
|
||||
const off = window.electronAPI?.onMenuAction?.((action) => {
|
||||
if (action === 'new-chat') {
|
||||
useSessionStore.getState().newSession()
|
||||
navigate('/')
|
||||
@@ -42,6 +42,7 @@ const App: React.FC = () => {
|
||||
navigate('/settings')
|
||||
}
|
||||
})
|
||||
return off
|
||||
}, [navigate])
|
||||
|
||||
const handleLangChange = useCallback(() => forceUpdate((n) => n + 1), [])
|
||||
|
||||
@@ -26,6 +26,7 @@ export function useBackend() {
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
let offStatus: (() => void) | undefined
|
||||
const api = window.electronAPI
|
||||
|
||||
const startPolling = async (port: number) => {
|
||||
@@ -62,7 +63,7 @@ export function useBackend() {
|
||||
startPolling(p)
|
||||
})
|
||||
|
||||
api.onBackendStatus((data) => {
|
||||
offStatus = api.onBackendStatus((data) => {
|
||||
if (data.status === 'ready' && data.port) {
|
||||
setState({ status: 'ready', port: data.port })
|
||||
if (pollingRef.current) {
|
||||
@@ -82,6 +83,7 @@ export function useBackend() {
|
||||
if (pollingRef.current) {
|
||||
clearTimeout(pollingRef.current)
|
||||
}
|
||||
offStatus?.()
|
||||
}
|
||||
}, [probeBackend])
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ const WindowControls: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
api?.windowIsMaximized().then(setMaximized)
|
||||
api?.onMaximizeChange(setMaximized)
|
||||
const off = api?.onMaximizeChange(setMaximized)
|
||||
return off
|
||||
}, [api])
|
||||
|
||||
if (api?.platform === 'darwin') return null
|
||||
|
||||
@@ -8,14 +8,15 @@ export interface ElectronAPI {
|
||||
restartBackend: () => Promise<boolean>
|
||||
selectDirectory: () => Promise<string | null>
|
||||
selectFile: (filters?: { name: string; extensions: string[] }[]) => Promise<string | null>
|
||||
onBackendStatus: (callback: (data: BackendStatusEvent) => void) => void
|
||||
onBackendLog: (callback: (line: string) => void) => void
|
||||
// Listener registrars return an unsubscribe fn for cleanup.
|
||||
onBackendStatus: (callback: (data: BackendStatusEvent) => void) => () => void
|
||||
onBackendLog: (callback: (line: string) => void) => () => void
|
||||
windowMinimize: () => Promise<void>
|
||||
windowMaximize: () => Promise<boolean>
|
||||
windowClose: () => Promise<void>
|
||||
windowIsMaximized: () => Promise<boolean>
|
||||
onMaximizeChange: (callback: (maximized: boolean) => void) => void
|
||||
onMenuAction?: (callback: (action: string) => void) => void
|
||||
onMaximizeChange: (callback: (maximized: boolean) => void) => () => void
|
||||
onMenuAction?: (callback: (action: string) => void) => () => void
|
||||
platform: string
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user