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
136 lines
4.0 KiB
TypeScript
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
|
|
}
|
|
}
|