import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'fs'; import { join } from 'path'; const LIFECYCLE_LOG_FILE = 'server-lifecycle.jsonl'; function getLogPath(__dirname) { const logsDir = join(__dirname, '../logs'); if (!existsSync(logsDir)) { mkdirSync(logsDir, { recursive: true }); } return join(logsDir, LIFECYCLE_LOG_FILE); } function appendEvent(__dirname, event) { const entry = { timestamp: new Date().toISOString(), ...event }; try { appendFileSync(getLogPath(__dirname), `${JSON.stringify(entry)}\n`, 'utf-8'); } catch (error) { console.error('[Lifecycle] Ereignis konnte nicht protokolliert werden:', error.message); } return entry; } function readEvents(__dirname) { try { const logPath = getLogPath(__dirname); if (!existsSync(logPath)) return []; return readFileSync(logPath, 'utf-8') .split('\n') .map((line) => line.trim()) .filter(Boolean) .map((line) => { try { return JSON.parse(line); } catch { return null; } }) .filter((event) => event && typeof event.timestamp === 'string' && typeof event.type === 'string'); } catch (error) { console.error('[Lifecycle] Protokoll konnte nicht gelesen werden:', error.message); return []; } } export function logServerStart(__dirname) { const events = readEvents(__dirname); const previous = events.at(-1); let reason = 'Erststart oder vorheriger Shutdown ist bereits protokolliert'; // SIGKILL, OOM und Stromausfälle geben dem Node-Prozess keine Gelegenheit, // einen Shutdown zu schreiben. Beim nächsten Start machen wir das transparent. if (previous?.type === 'start') { appendEvent(__dirname, { type: 'stop', reason: 'Abrupte Beendigung: kein sauberer Shutdown protokolliert', previousPid: previous.pid }); reason = 'Neustart nach abrupter Beendigung der vorherigen Instanz'; } else if (previous?.type === 'stop' && previous.reason?.startsWith('Kontrollierter Shutdown')) { reason = `Kontrollierter Neustart nach: ${previous.reason}`; } return appendEvent(__dirname, { type: 'start', pid: process.pid, reason }); } export function logServerStop(__dirname, reason, details = {}) { return appendEvent(__dirname, { type: 'stop', pid: process.pid, reason, ...details }); } export function getServerStartStats(__dirname, limit = 5) { const starts = readEvents(__dirname).filter((event) => event.type === 'start'); return { count: starts.length, first: starts[0] || null, last: starts.at(-1) || null, latest: starts.slice(-limit).reverse() }; } export function installLifecycleHandlers(__dirname, httpServer) { let shuttingDown = false; const gracefulShutdown = (signal) => { if (shuttingDown) return; shuttingDown = true; logServerStop( __dirname, `Kontrollierter Shutdown per ${signal} (manuell, Deployment oder System-Neustart)` ); httpServer.close(() => process.exit(0)); setTimeout(() => process.exit(0), 10_000).unref(); }; process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); process.on('SIGINT', () => gracefulShutdown('SIGINT')); process.on('uncaughtException', (error) => { logServerStop(__dirname, 'Absturz durch unbehandelte Ausnahme', { error: String(error?.stack || error?.message || error) }); console.error('[Lifecycle] Unbehandelte Ausnahme:', error); process.exit(1); }); process.on('unhandledRejection', (reason) => { logServerStop(__dirname, 'Absturz durch unbehandelte Promise-Ablehnung', { error: String(reason?.stack || reason?.message || reason) }); console.error('[Lifecycle] Unbehandelte Promise-Ablehnung:', reason); process.exit(1); }); }