Files
Codewalkers/apps/server/server/shutdown.ts
Lukas May 34578d39c6 refactor: Restructure monorepo to apps/server/ and apps/web/ layout
Move src/ → apps/server/ and packages/web/ → apps/web/ to adopt
standard monorepo conventions (apps/ for runnable apps, packages/
for reusable libraries). Update all config files, shared package
imports, test fixtures, and documentation to reflect new paths.

Key fixes:
- Update workspace config to ["apps/*", "packages/*"]
- Update tsconfig.json rootDir/include for apps/server/
- Add apps/web/** to vitest exclude list
- Update drizzle.config.ts schema path
- Fix ensure-schema.ts migration path detection (3 levels up in dev,
  2 levels up in dist)
- Fix tests/integration/cli-server.test.ts import paths
- Update packages/shared imports to apps/server/ paths
- Update all docs/ files with new paths
2026-03-03 11:22:53 +01:00

136 lines
4.0 KiB
TypeScript

/**
* Graceful Shutdown Handler
*
* Orchestrates clean shutdown of the coordination server.
* Handles SIGTERM, SIGINT, SIGHUP with proper cleanup sequence.
*/
import type { CoordinationServer } from './index.js';
import type { ProcessManager } from '../process/index.js';
import type { LogManager } from '../logging/index.js';
import type { PreviewManager } from '../preview/index.js';
/** Timeout before force exit in milliseconds */
const SHUTDOWN_TIMEOUT_MS = 10000;
/**
* Handles graceful shutdown of the coordination server.
*
* Shutdown sequence:
* 1. Log shutdown initiation
* 2. Stop accepting new connections
* 3. Stop all managed processes
* 4. Close all log file handles (not implemented yet in LogManager)
* 5. Remove PID file
* 6. Exit with code 0
*
* If cleanup takes longer than SHUTDOWN_TIMEOUT_MS, force exit with code 1.
* Double SIGINT forces immediate exit.
*/
export class GracefulShutdown {
private readonly server: CoordinationServer;
private readonly processManager: ProcessManager;
private readonly logManager: LogManager;
private readonly previewManager?: PreviewManager;
private isShuttingDown = false;
private forceExitCount = 0;
constructor(
server: CoordinationServer,
processManager: ProcessManager,
logManager: LogManager,
previewManager?: PreviewManager,
) {
this.server = server;
this.processManager = processManager;
this.logManager = logManager;
this.previewManager = previewManager;
}
/**
* Installs signal handlers for graceful shutdown.
* Call this after the server has started.
*/
install(): void {
// Handle SIGTERM (kill, docker stop)
process.on('SIGTERM', () => this.handleSignal('SIGTERM'));
// Handle SIGINT (Ctrl+C)
process.on('SIGINT', () => this.handleSignal('SIGINT'));
// Handle SIGHUP (terminal closed)
process.on('SIGHUP', () => this.handleSignal('SIGHUP'));
}
/**
* Handles a shutdown signal.
* @param signal - The signal that triggered shutdown
*/
private async handleSignal(signal: string): Promise<void> {
// Handle double SIGINT for force exit
if (signal === 'SIGINT' && this.isShuttingDown) {
this.forceExitCount++;
if (this.forceExitCount >= 1) {
console.log('\nForce exit requested. Exiting immediately.');
process.exit(1);
}
}
// Only run shutdown sequence once
if (this.isShuttingDown) {
console.log(`\nAlready shutting down... Press Ctrl+C again to force exit.`);
return;
}
this.isShuttingDown = true;
console.log(`\nReceived ${signal}, shutting down gracefully...`);
// Set timeout for force exit
const forceExitTimer = setTimeout(() => {
console.error('Shutdown timeout exceeded. Forcing exit.');
process.exit(1);
}, SHUTDOWN_TIMEOUT_MS);
// Ensure timeout doesn't keep the process alive
forceExitTimer.unref();
try {
await this.shutdown();
clearTimeout(forceExitTimer);
console.log('Shutdown complete.');
process.exit(0);
} catch (error) {
clearTimeout(forceExitTimer);
console.error('Error during shutdown:', (error as Error).message);
process.exit(1);
}
}
/**
* Performs the shutdown sequence.
*/
async shutdown(): Promise<void> {
// Step 1: Stop the HTTP server (stops accepting new connections)
console.log(' Stopping HTTP server...');
await this.server.stop();
// Step 2: Stop all managed processes
console.log(' Stopping managed processes...');
await this.processManager.stopAll();
// Step 3: Stop all preview deployments
if (this.previewManager) {
console.log(' Stopping preview deployments...');
await this.previewManager.stopAll();
}
// Step 4: Clean up log manager resources (future: close open file handles)
// Currently LogManager doesn't maintain persistent handles that need closing
// This is a placeholder for future cleanup needs
console.log(' Cleaning up resources...');
// Step 4: PID file is removed by server.stop()
// Nothing additional needed here
}
}