2026-04-21 12:35:48 +08:00
|
|
|
import { logger } from './logger'
|
2026-05-07 13:50:33 +08:00
|
|
|
import { closeDb } from '../db'
|
2026-05-11 20:08:13 +08:00
|
|
|
import { getGatewayManagerInstance } from './gateway-bootstrap'
|
|
|
|
|
|
|
|
|
|
function shouldStopGatewaysOnShutdown(signal: string): boolean {
|
2026-05-12 21:17:40 +08:00
|
|
|
// nodemon may use SIGTERM on Windows restarts, so dev mode opts out via env.
|
|
|
|
|
// Production keeps stopping owned gateways by default.
|
2026-05-11 20:08:13 +08:00
|
|
|
const override = process.env.HERMES_WEB_UI_STOP_GATEWAYS_ON_SHUTDOWN?.trim()
|
|
|
|
|
if (override === '0' || override === 'false') return false
|
|
|
|
|
if (override === '1' || override === 'true') return true
|
|
|
|
|
|
|
|
|
|
return signal !== 'SIGUSR2'
|
|
|
|
|
}
|
2026-04-21 12:35:48 +08:00
|
|
|
|
2026-05-05 13:03:14 +08:00
|
|
|
export function bindShutdown(server: any, groupChatServer?: any, chatRunServer?: any): void {
|
2026-04-20 20:37:32 +08:00
|
|
|
let isShuttingDown = false
|
|
|
|
|
|
|
|
|
|
const shutdown = async (signal: string) => {
|
|
|
|
|
if (isShuttingDown) return
|
|
|
|
|
isShuttingDown = true
|
|
|
|
|
|
2026-05-07 19:11:32 +08:00
|
|
|
// Force exit after 3s no matter what
|
|
|
|
|
setTimeout(() => process.exit(0), 3000)
|
|
|
|
|
|
2026-04-21 12:35:48 +08:00
|
|
|
logger.info('Shutting down (%s)...', signal)
|
2026-04-20 20:37:32 +08:00
|
|
|
|
|
|
|
|
try {
|
2026-05-11 20:08:13 +08:00
|
|
|
if (shouldStopGatewaysOnShutdown(signal)) {
|
|
|
|
|
// Stop gateway processes owned by this Web UI instance first.
|
|
|
|
|
try {
|
|
|
|
|
const gatewayManager = getGatewayManagerInstance()
|
|
|
|
|
if (gatewayManager) {
|
|
|
|
|
await gatewayManager.stopAll()
|
|
|
|
|
logger.info('All gateways stopped')
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
logger.warn(err, 'Failed to stop gateways (non-fatal)')
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
logger.info('Skipping gateway shutdown for %s', signal)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-05 13:03:14 +08:00
|
|
|
// Close ChatRunSocket first to abort all active runs and close EventSource connections
|
|
|
|
|
if (chatRunServer) {
|
|
|
|
|
chatRunServer.close()
|
|
|
|
|
logger.info('ChatRunSocket closed')
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 20:41:14 +08:00
|
|
|
// Disconnect Socket.IO before HTTP server to prevent hanging
|
|
|
|
|
if (groupChatServer) {
|
|
|
|
|
groupChatServer.agentClients.disconnectAll()
|
|
|
|
|
groupChatServer.getIO().close()
|
|
|
|
|
logger.info('Socket.IO closed')
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-07 19:11:32 +08:00
|
|
|
const servers = Array.isArray(server) ? server : [server].filter(Boolean)
|
|
|
|
|
if (servers.length) {
|
|
|
|
|
await Promise.all(servers.map((httpServer) => (
|
|
|
|
|
new Promise<void>((resolve) => {
|
|
|
|
|
httpServer.close(() => {
|
|
|
|
|
logger.info('HTTP server closed')
|
|
|
|
|
resolve()
|
|
|
|
|
})
|
2026-04-20 20:37:32 +08:00
|
|
|
})
|
2026-05-07 19:11:32 +08:00
|
|
|
)))
|
2026-04-20 20:37:32 +08:00
|
|
|
}
|
|
|
|
|
} catch (err) {
|
2026-04-21 12:35:48 +08:00
|
|
|
logger.error(err, 'Shutdown error')
|
2026-04-20 20:37:32 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-07 13:50:33 +08:00
|
|
|
closeDb()
|
2026-04-20 20:37:32 +08:00
|
|
|
process.exit(0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
process.once('SIGUSR2', shutdown)
|
|
|
|
|
process.on('SIGINT', shutdown)
|
|
|
|
|
process.on('SIGTERM', shutdown)
|
|
|
|
|
}
|