# All files / src/server shutdown.ts

37.2% Statements 16/43
10% Branches 1/10
37.5% Functions 3/8
40% Lines 16/40

Press n or j to go to the next uncovered block, b, p or k for the previous block.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136

    1x

    6x
    6x

    6x
    6x
    6x
    6x

    3x

    3x

    3x

    3x
    3x

    3x
    3x

    3x

    3x

     /**
     * 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
        Iif (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
      }
    }