- Drop all desktop/Electron branches, scripts, and UI (title bar, pets, desktop bridge); LAN endpoint kind is now web/custom only - Replace the bundled hermes CLI with user-side installation: add GET /api/hermes/cli-status plus a HermesMissingView onboarding guide - Remove the version-management UI (snapshots handled by git) and prune dead website/mcu/claude scripts and lockfile entries
130 lines
4.5 KiB
TypeScript
130 lines
4.5 KiB
TypeScript
import { logger } from './logger'
|
|
import { closeDb } from '../db'
|
|
import { stopPreviewRuntime } from '../controllers/update'
|
|
import { shutdownManagedGateways } from './hermes/gateway-runner'
|
|
import { stopOutboundRelayClient } from './global-agent/outbound-relay-client'
|
|
|
|
const DEFAULT_SHUTDOWN_FORCE_EXIT_MS = 15_000
|
|
|
|
function envPositiveInt(name: string): number | undefined {
|
|
const value = Number(process.env[name])
|
|
return Number.isFinite(value) && value > 0 ? value : undefined
|
|
}
|
|
|
|
export function getShutdownForceExitMs(): number {
|
|
const override = envPositiveInt('HERMES_WEB_UI_SHUTDOWN_FORCE_EXIT_MS')
|
|
if (override) return override
|
|
return DEFAULT_SHUTDOWN_FORCE_EXIT_MS
|
|
}
|
|
|
|
export function shouldStopAgentBridgeOnShutdown(_signal: string): boolean {
|
|
const raw = String(process.env.HERMES_AGENT_BRIDGE_STOP_ON_SHUTDOWN || '').trim().toLowerCase()
|
|
if (['1', 'true', 'yes', 'on'].includes(raw)) return true
|
|
if (['0', 'false', 'no', 'off'].includes(raw)) return false
|
|
|
|
// Restart now defaults to a fresh bridge so package/runtime upgrades do not
|
|
// attach to stale brokers. Operators can opt back into restart resume with
|
|
// HERMES_AGENT_BRIDGE_STOP_ON_SHUTDOWN=0.
|
|
return true
|
|
}
|
|
|
|
export function shouldStopManagedGatewaysOnShutdown(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
const raw = String(env.HERMES_WEB_UI_STOP_GATEWAYS_ON_SHUTDOWN || '').trim().toLowerCase()
|
|
if (['1', 'true', 'yes', 'on'].includes(raw)) return true
|
|
if (['0', 'false', 'no', 'off'].includes(raw)) return false
|
|
|
|
return String(env.NODE_ENV || '').trim().toLowerCase() === 'production'
|
|
}
|
|
|
|
export type ShutdownHandler = (signal: string) => Promise<void>
|
|
|
|
export function createShutdownHandler(server: any, groupChatServer?: any, chatRunServer?: any, agentBridgeManager?: any): ShutdownHandler {
|
|
let isShuttingDown = false
|
|
|
|
return async (signal: string) => {
|
|
if (isShuttingDown) return
|
|
isShuttingDown = true
|
|
|
|
// Force exit only if graceful cleanup hangs. The bridge can take up to 10s
|
|
// to stop worker subprocesses, so this cap must be longer than that.
|
|
setTimeout(() => process.exit(0), getShutdownForceExitMs())
|
|
|
|
logger.info('Shutting down (%s)...', signal)
|
|
console.log(`[shutdown] Received signal: ${signal}`)
|
|
|
|
try {
|
|
try {
|
|
await stopPreviewRuntime()
|
|
logger.info('Preview runtime stopped')
|
|
} catch (err) {
|
|
logger.warn(err, 'Failed to stop preview runtime (non-fatal)')
|
|
}
|
|
|
|
if (shouldStopManagedGatewaysOnShutdown()) {
|
|
try {
|
|
const result = await shutdownManagedGateways()
|
|
logger.info('[shutdown] managed gateways stopped result=%j', result)
|
|
} catch (err) {
|
|
logger.warn(err, 'Failed to stop managed gateways (non-fatal)')
|
|
}
|
|
} else {
|
|
logger.info('[shutdown] leaving managed gateways running')
|
|
}
|
|
|
|
if (agentBridgeManager && shouldStopAgentBridgeOnShutdown(signal)) {
|
|
try {
|
|
await agentBridgeManager.stop()
|
|
logger.info('Agent bridge stopped')
|
|
} catch (err) {
|
|
logger.warn(err, 'Failed to stop agent bridge (non-fatal)')
|
|
}
|
|
} else if (agentBridgeManager) {
|
|
logger.info('Leaving agent bridge running across Web UI shutdown')
|
|
}
|
|
|
|
// Close ChatRunSocket first to release WebSocket state.
|
|
if (chatRunServer) {
|
|
chatRunServer.close()
|
|
logger.info('ChatRunSocket closed')
|
|
}
|
|
|
|
stopOutboundRelayClient()
|
|
logger.info('Outbound relay clients closed')
|
|
|
|
// Disconnect Socket.IO before HTTP server to prevent hanging
|
|
if (groupChatServer) {
|
|
groupChatServer.agentClients.disconnectAll()
|
|
groupChatServer.getIO().close()
|
|
logger.info('Socket.IO closed')
|
|
}
|
|
|
|
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()
|
|
})
|
|
})
|
|
)))
|
|
}
|
|
} catch (err) {
|
|
logger.error(err, 'Shutdown error')
|
|
}
|
|
|
|
closeDb()
|
|
process.exit(0)
|
|
}
|
|
}
|
|
|
|
export function bindShutdown(server: any, groupChatServer?: any, chatRunServer?: any, agentBridgeManager?: any): ShutdownHandler {
|
|
const shutdown = createShutdownHandler(server, groupChatServer, chatRunServer, agentBridgeManager)
|
|
|
|
process.once('SIGUSR2', shutdown)
|
|
process.on('SIGINT', shutdown)
|
|
process.on('SIGTERM', shutdown)
|
|
|
|
return shutdown
|
|
}
|