112 lines
3.7 KiB
JavaScript
112 lines
3.7 KiB
JavaScript
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);
|
|
});
|
|
}
|