diff --git a/README.md b/README.md index fc45e26..72d95f8 100644 --- a/README.md +++ b/README.md @@ -1066,18 +1066,18 @@ const result4 = await $`echo "pipe test"`.pipe($`cat`); const text4 = await result4.text(); // "pipe test\n" ``` -## Signal Handling (CTRL+C Support) +## Signal Handling (CTRL+C, SIGTERM, and Other Signals) -The library provides **advanced CTRL+C handling** that properly manages signals across different scenarios: +The library provides **comprehensive signal handling** that properly manages SIGINT (CTRL+C), SIGTERM, SIGKILL, and other signals across different scenarios: ### How It Works -1. **Smart Signal Forwarding**: CTRL+C is forwarded **only when child processes are active** -2. **User Handler Preservation**: When no children are running, your custom SIGINT handlers work normally +1. **Smart Signal Forwarding**: Signals are forwarded **only when child processes are active** +2. **User Handler Preservation**: When no children are running, your custom signal handlers work normally 3. **Process Groups**: Child processes use detached spawning for proper signal isolation 4. **TTY Mode Support**: Raw TTY mode is properly managed and restored on interruption 5. **Graceful Termination**: Uses SIGTERM → SIGKILL escalation for robust process cleanup -6. **Exit Code Standards**: Proper signal exit codes (130 for SIGINT, 143 for SIGTERM) +6. **Exit Code Standards**: Proper signal exit codes (130 for SIGINT, 143 for SIGTERM, 137 for SIGKILL) ### Advanced Signal Behavior @@ -1134,17 +1134,248 @@ try { } ``` +### Sending Signals to Commands + +#### Programmatic Signal Control + +You can send different signals to running commands using the `kill()` method: + +```javascript +import { $ } from 'command-stream'; + +// Start a long-running command +const runner = $`sleep 30`; +const promise = runner.start(); // Non-blocking start + +// Send different signals after some time: + +// 1. SIGTERM (15) - Polite termination request (default) +setTimeout(() => { + runner.kill(); // Default: SIGTERM + // or explicitly: + runner.kill('SIGTERM'); +}, 5000); + +// 2. SIGINT (2) - Interrupt signal (same as CTRL+C) +setTimeout(() => { + runner.kill('SIGINT'); +}, 3000); + +// 3. SIGKILL (9) - Force termination (cannot be caught) +setTimeout(() => { + runner.kill('SIGKILL'); +}, 10000); + +// 4. SIGUSR1 (10) - User-defined signal 1 +setTimeout(() => { + runner.kill('SIGUSR1'); +}, 7000); + +// 5. SIGUSR2 (12) - User-defined signal 2 +setTimeout(() => { + runner.kill('SIGUSR2'); +}, 8000); + +// Wait for command completion and check exit code +try { + const result = await promise; + console.log('Exit code:', result.code); +} catch (error) { + console.log('Command terminated:', error.code); +} +``` + +#### Signal Exit Codes + +Different signals produce specific exit codes: + +```javascript +import { $ } from 'command-stream'; + +// Test different signal exit codes +async function testSignalExitCodes() { + // SIGINT (CTRL+C) → Exit code 130 (128 + 2) + const runner1 = $`sleep 5`; + const promise1 = runner1.start(); + setTimeout(() => runner1.kill('SIGINT'), 1000); + const result1 = await promise1; + console.log('SIGINT exit code:', result1.code); // → 130 + + // SIGTERM → Exit code 143 (128 + 15) + const runner2 = $`sleep 5`; + const promise2 = runner2.start(); + setTimeout(() => runner2.kill('SIGTERM'), 1000); + const result2 = await promise2; + console.log('SIGTERM exit code:', result2.code); // → 143 + + // SIGKILL → Exit code 137 (128 + 9) + const runner3 = $`sleep 5`; + const promise3 = runner3.start(); + setTimeout(() => runner3.kill('SIGKILL'), 1000); + const result3 = await promise3; + console.log('SIGKILL exit code:', result3.code); // → 137 +} +``` + +#### Graceful Shutdown Patterns + +Implement graceful shutdown with escalating signals: + +```javascript +import { $ } from 'command-stream'; + +async function gracefulShutdown(runner, timeoutMs = 5000) { + console.log('Requesting graceful shutdown with SIGTERM...'); + + // Step 1: Send SIGTERM (polite request) + runner.kill('SIGTERM'); + + // Step 2: Wait for graceful shutdown + const shutdownTimeout = setTimeout(() => { + console.log('Graceful shutdown timeout, sending SIGKILL...'); + runner.kill('SIGKILL'); // Force termination + }, timeoutMs); + + try { + const result = await runner; + clearTimeout(shutdownTimeout); + console.log('Process exited gracefully:', result.code); + return result; + } catch (error) { + clearTimeout(shutdownTimeout); + console.log('Process terminated:', error.code); + throw error; + } +} + +// Usage example +const longRunningProcess = $`node server.js`; +longRunningProcess.start(); + +// Later, when you need to shut down: +await gracefulShutdown(longRunningProcess, 10000); // 10 second timeout +``` + +#### Interactive Command Termination + +Handle interactive commands that ignore stdin but respond to signals: + +```javascript +import { $ } from 'command-stream'; + +// Commands like ping ignore stdin but respond to signals +async function runPingWithTimeout(host, timeoutSeconds = 5) { + const pingRunner = $`ping ${host}`; + const promise = pingRunner.start(); + + // Set up timeout to send SIGINT after specified time + const timeoutId = setTimeout(() => { + console.log(`Stopping ping after ${timeoutSeconds} seconds...`); + pingRunner.kill('SIGINT'); // Same as pressing CTRL+C + }, timeoutSeconds * 1000); + + try { + const result = await promise; + clearTimeout(timeoutId); + return result; + } catch (error) { + clearTimeout(timeoutId); + console.log('Ping interrupted with exit code:', error.code); // Usually 130 + return error; + } +} + +// Run ping for 3 seconds then automatically stop +await runPingWithTimeout('8.8.8.8', 3); +``` + +#### Multiple Process Signal Management + +Send signals to multiple concurrent processes: + +```javascript +import { $ } from 'command-stream'; + +async function runMultipleWithSignalControl() { + // Start multiple long-running processes + const processes = [ + $`tail -f /var/log/system.log`, + $`ping google.com`, + $`sleep 60`, + ]; + + // Start all processes + const promises = processes.map(p => p.start()); + + // After 10 seconds, send SIGTERM to all + setTimeout(() => { + console.log('Sending SIGTERM to all processes...'); + processes.forEach(p => p.kill('SIGTERM')); + }, 10000); + + // After 15 seconds, send SIGKILL to any survivors + setTimeout(() => { + console.log('Sending SIGKILL to remaining processes...'); + processes.forEach(p => p.kill('SIGKILL')); + }, 15000); + + // Wait for all to complete + const results = await Promise.allSettled(promises); + results.forEach((result, index) => { + if (result.status === 'fulfilled') { + console.log(`Process ${index} exit code:`, result.value.code); + } else { + console.log(`Process ${index} error:`, result.reason.code); + } + }); +} +``` + ### Signal Handling Behavior -- **🎯 Smart Detection**: Only forwards CTRL+C when child processes are active -- **🛡️ Non-Interference**: Preserves user SIGINT handlers when no children running +- **🎯 Smart Detection**: Only forwards signals when child processes are active +- **🛡️ Non-Interference**: Preserves user signal handlers when no children running - **⚡ Interactive Commands**: Use `interactive: true` option for commands like `vim`, `less`, `top` to enable proper TTY forwarding and signal handling - **🔄 Process Groups**: Detached spawning ensures proper signal isolation - **🧹 TTY Cleanup**: Raw terminal mode properly restored on interruption +- **⚖️ Signal Escalation**: Supports SIGTERM → SIGKILL escalation for robust cleanup +- **🔀 Signal Forwarding**: All standard Unix signals can be forwarded to child processes - **📊 Standard Exit Codes**: - - `130` - SIGINT interruption (CTRL+C) - - `143` - SIGTERM termination (programmatic kill) - - `137` - SIGKILL force termination + - `130` - SIGINT interruption (CTRL+C) - Signal number 2 + - `143` - SIGTERM termination (programmatic kill) - Signal number 15 + - `137` - SIGKILL force termination - Signal number 9 + - `128 + N` - General formula for signal exit codes (where N is signal number) + +### Available Signals + +The library supports all standard Unix signals: + +| Signal | Number | Description | Can be caught? | Common use case | +|--------|--------|-------------|----------------|-----------------| +| `SIGINT` | 2 | Interrupt (CTRL+C) | ✅ Yes | User interrupt | +| `SIGTERM` | 15 | Terminate (default kill) | ✅ Yes | Graceful shutdown | +| `SIGKILL` | 9 | Kill | ❌ No | Force termination | +| `SIGQUIT` | 3 | Quit with core dump | ✅ Yes | Debug termination | +| `SIGHUP` | 1 | Hang up | ✅ Yes | Reload configuration | +| `SIGUSR1` | 10 | User signal 1 | ✅ Yes | Custom application logic | +| `SIGUSR2` | 12 | User signal 2 | ✅ Yes | Custom application logic | +| `SIGPIPE` | 13 | Broken pipe | ✅ Yes | Pipe communication error | +| `SIGALRM` | 14 | Alarm clock | ✅ Yes | Timer expiration | +| `SIGSTOP` | 19 | Stop process | ❌ No | Pause execution | +| `SIGCONT` | 18 | Continue process | ✅ Yes | Resume execution | + +**Usage examples:** + +```javascript +// All these signals can be sent to running commands: +runner.kill('SIGINT'); // Interrupt (same as CTRL+C) +runner.kill('SIGTERM'); // Graceful termination (default) +runner.kill('SIGKILL'); // Force kill +runner.kill('SIGHUP'); // Hang up +runner.kill('SIGUSR1'); // User-defined signal 1 +runner.kill('SIGUSR2'); // User-defined signal 2 +runner.kill('SIGQUIT'); // Quit with core dump +``` ### Command Resolution Priority diff --git a/examples/ctrl-c-vs-sigterm.mjs b/examples/ctrl-c-vs-sigterm.mjs new file mode 100644 index 0000000..cf37cfd --- /dev/null +++ b/examples/ctrl-c-vs-sigterm.mjs @@ -0,0 +1,191 @@ +#!/usr/bin/env node + +/** + * CTRL+C vs SIGTERM Comparison + * + * Demonstrates the differences between: + * - CTRL+C (SIGINT) - User interrupt signal + * - SIGTERM - Termination request signal (default for kill command) + * + * Both can be caught by processes, but they have different semantic meanings. + * + * Usage: + * node examples/ctrl-c-vs-sigterm.mjs + */ + +import { $ } from '../src/$.mjs'; + +console.log('🔄 CTRL+C (SIGINT) vs SIGTERM Comparison\n'); + +async function demonstrateSignalDifferences() { + console.log('Understanding the difference between SIGINT and SIGTERM:\n'); + + console.log('📝 SIGINT (Signal 2) - "Interrupt" - Usually CTRL+C'); + console.log(' • Semantic meaning: User wants to interrupt/cancel'); + console.log(' • Common sources: Terminal CTRL+C, kill -2, kill -INT'); + console.log(' • Exit code when caught: Usually 130 (128 + 2)'); + console.log(' • Can be caught and handled by programs'); + + console.log('\n📝 SIGTERM (Signal 15) - "Terminate" - Default kill signal'); + console.log(' • Semantic meaning: Request to terminate gracefully'); + console.log(' • Common sources: kill command (default), systemd, process managers'); + console.log(' • Exit code when caught: Usually 143 (128 + 15)'); + console.log(' • Can be caught and handled by programs'); + + console.log('\n' + '─'.repeat(50) + '\n'); +} + +async function testSigintBehavior() { + console.log('🧪 Testing SIGINT (CTRL+C equivalent) behavior:'); + + try { + const runner = $`sleep 5`; + const promise = runner.start(); + + // Send SIGINT after 1 second + setTimeout(() => { + console.log(' 📤 Sending SIGINT (equivalent to pressing CTRL+C)...'); + runner.kill('SIGINT'); + }, 1000); + + const result = await promise; + console.log(' ✓ SIGINT result - Exit code:', result.code, '(should be 130)'); + } catch (error) { + console.log(' ✓ SIGINT interrupted with exit code:', error.code); + } +} + +async function testSigtermBehavior() { + console.log('\n🧪 Testing SIGTERM (default kill) behavior:'); + + try { + const runner = $`sleep 5`; + const promise = runner.start(); + + // Send SIGTERM after 1 second + setTimeout(() => { + console.log(' 📤 Sending SIGTERM (default kill signal)...'); + runner.kill('SIGTERM'); // or just runner.kill() - SIGTERM is default + }, 1000); + + const result = await promise; + console.log(' ✓ SIGTERM result - Exit code:', result.code, '(should be 143)'); + } catch (error) { + console.log(' ✓ SIGTERM terminated with exit code:', error.code); + } +} + +async function testDefaultKillBehavior() { + console.log('\n🧪 Testing default kill() behavior (should be SIGTERM):'); + + try { + const runner = $`sleep 5`; + const promise = runner.start(); + + // Default kill (should send SIGTERM) + setTimeout(() => { + console.log(' 📤 Calling kill() without signal (defaults to SIGTERM)...'); + runner.kill(); // No signal specified - should default to SIGTERM + }, 1000); + + const result = await promise; + console.log(' ✓ Default kill() result - Exit code:', result.code, '(should be 143 for SIGTERM)'); + } catch (error) { + console.log(' ✓ Default kill() terminated with exit code:', error.code); + } +} + +async function demonstrateExitCodes() { + console.log('\n📊 Exit Code Demonstration:'); + console.log('Formula: Exit Code = 128 + Signal Number'); + console.log('• SIGINT (2): 128 + 2 = 130'); + console.log('• SIGTERM (15): 128 + 15 = 143'); + console.log('• SIGKILL (9): 128 + 9 = 137'); + + const signals = [ + { name: 'SIGINT', number: 2, expected: 130 }, + { name: 'SIGTERM', number: 15, expected: 143 }, + { name: 'SIGKILL', number: 9, expected: 137 } + ]; + + console.log('\n🧮 Testing exit code formula:'); + + for (const signal of signals) { + try { + const runner = $`sleep 3`; + const promise = runner.start(); + + setTimeout(() => { + console.log(` 📤 Sending ${signal.name}...`); + runner.kill(signal.name); + }, 500); + + const result = await promise; + const match = result.code === signal.expected ? '✅' : '❌'; + console.log(` ${match} ${signal.name} → Exit code: ${result.code} (expected: ${signal.expected})`); + } catch (error) { + const match = error.code === signal.expected ? '✅' : '❌'; + console.log(` ${match} ${signal.name} → Exit code: ${error.code} (expected: ${signal.expected})`); + } + } +} + +async function demonstrateRealWorldUsage() { + console.log('\n🌍 Real-world Usage Examples:'); + + console.log('\n1️⃣ User interruption (CTRL+C equivalent):'); + console.log(' Use SIGINT when user wants to cancel/interrupt'); + + // Simulate user pressing CTRL+C + const pingRunner = $`ping -c 10 8.8.8.8`; + const pingPromise = pingRunner.start(); + + setTimeout(() => { + console.log(' 👤 User pressed CTRL+C - sending SIGINT...'); + pingRunner.kill('SIGINT'); // User interruption + }, 2000); + + try { + await pingPromise; + } catch (error) { + console.log(' ✓ Ping interrupted by user, exit code:', error.code); + } + + console.log('\n2️⃣ System shutdown (graceful termination):'); + console.log(' Use SIGTERM for graceful shutdown requests'); + + // Simulate system requesting graceful shutdown + const serverRunner = $`sleep 8`; // Simulate server process + const serverPromise = serverRunner.start(); + + setTimeout(() => { + console.log(' 🏭 System requesting graceful shutdown - sending SIGTERM...'); + serverRunner.kill('SIGTERM'); // System shutdown + }, 1000); + + try { + await serverPromise; + } catch (error) { + console.log(' ✓ Server gracefully terminated, exit code:', error.code); + } +} + +async function main() { + await demonstrateSignalDifferences(); + await testSigintBehavior(); + await testSigtermBehavior(); + await testDefaultKillBehavior(); + await demonstrateExitCodes(); + await demonstrateRealWorldUsage(); + + console.log('\n🎉 CTRL+C vs SIGTERM Comparison completed!'); + console.log('\nKey Takeaways:'); + console.log('• SIGINT (CTRL+C): User interruption → Exit code 130'); + console.log('• SIGTERM: Graceful termination → Exit code 143'); + console.log('• Both can be caught and handled by processes'); + console.log('• Default kill() sends SIGTERM, not SIGINT'); + console.log('• Exit codes follow formula: 128 + signal number'); + console.log('• Choose signal based on semantic meaning, not just functionality'); +} + +main().catch(console.error); \ No newline at end of file diff --git a/examples/signal-handling-demo.mjs b/examples/signal-handling-demo.mjs new file mode 100644 index 0000000..f24fa97 --- /dev/null +++ b/examples/signal-handling-demo.mjs @@ -0,0 +1,191 @@ +#!/usr/bin/env node + +/** + * Signal Handling Demonstration + * + * This example demonstrates how to send different signals (SIGTERM, SIGINT, SIGKILL, etc.) + * to executed commands using the command-stream library. + * + * Usage: + * node examples/signal-handling-demo.mjs + */ + +import { $ } from '../src/$.mjs'; + +console.log('🔧 Signal Handling Demo - Various signal types\n'); + +async function demoSignalTypes() { + console.log('1. SIGINT (CTRL+C) Example - Exit code 130'); + + try { + const runner1 = $`sleep 5`; + const promise1 = runner1.start(); + + // Send SIGINT after 1 second + setTimeout(() => { + console.log(' 📡 Sending SIGINT...'); + runner1.kill('SIGINT'); + }, 1000); + + const result1 = await promise1; + console.log(' ✓ Exit code:', result1.code); // Should be 130 + } catch (error) { + console.log(' ✓ Command interrupted with exit code:', error.code); + } + + console.log('\n2. SIGTERM (Graceful termination) Example - Exit code 143'); + + try { + const runner2 = $`sleep 5`; + const promise2 = runner2.start(); + + // Send SIGTERM after 1 second + setTimeout(() => { + console.log(' 📡 Sending SIGTERM...'); + runner2.kill('SIGTERM'); // or just runner2.kill() - SIGTERM is default + }, 1000); + + const result2 = await promise2; + console.log(' ✓ Exit code:', result2.code); // Should be 143 + } catch (error) { + console.log(' ✓ Command terminated with exit code:', error.code); + } + + console.log('\n3. SIGKILL (Force termination) Example - Exit code 137'); + + try { + const runner3 = $`sleep 5`; + const promise3 = runner3.start(); + + // Send SIGKILL after 1 second + setTimeout(() => { + console.log(' 📡 Sending SIGKILL...'); + runner3.kill('SIGKILL'); + }, 1000); + + const result3 = await promise3; + console.log(' ✓ Exit code:', result3.code); // Should be 137 + } catch (error) { + console.log(' ✓ Command force-killed with exit code:', error.code); + } +} + +async function demoGracefulShutdown() { + console.log('\n4. Graceful Shutdown Pattern (SIGTERM → SIGKILL escalation)'); + + async function gracefulShutdown(runner, timeoutMs = 3000) { + console.log(' 📡 Requesting graceful shutdown with SIGTERM...'); + + // Step 1: Send SIGTERM (polite request) + runner.kill('SIGTERM'); + + // Step 2: Wait for graceful shutdown with timeout + const shutdownTimeout = setTimeout(() => { + console.log(' ⏰ Graceful shutdown timeout, sending SIGKILL...'); + runner.kill('SIGKILL'); // Force termination + }, timeoutMs); + + try { + const result = await runner; + clearTimeout(shutdownTimeout); + console.log(' ✓ Process exited gracefully with code:', result.code); + return result; + } catch (error) { + clearTimeout(shutdownTimeout); + console.log(' ✓ Process terminated with code:', error.code); + return error; + } + } + + const runner = $`sleep 10`; + runner.start(); + + // Wait 1 second then try graceful shutdown + setTimeout(() => { + gracefulShutdown(runner, 2000); // 2 second timeout for demo + }, 1000); +} + +async function demoInteractiveCommandTermination() { + console.log('\n5. Interactive Command Termination (ping example)'); + + // Commands like ping ignore stdin but respond to signals + async function runPingWithTimeout(host, timeoutSeconds = 3) { + console.log(` 📡 Starting ping to ${host} for ${timeoutSeconds} seconds...`); + + const pingRunner = $`ping ${host}`; + const promise = pingRunner.start(); + + // Set up timeout to send SIGINT after specified time + const timeoutId = setTimeout(() => { + console.log(` ⏰ Stopping ping after ${timeoutSeconds} seconds...`); + pingRunner.kill('SIGINT'); // Same as pressing CTRL+C + }, timeoutSeconds * 1000); + + try { + const result = await promise; + clearTimeout(timeoutId); + console.log(' ✓ Ping completed naturally with exit code:', result.code); + return result; + } catch (error) { + clearTimeout(timeoutId); + console.log(' ✓ Ping interrupted with exit code:', error.code); // Usually 130 + return error; + } + } + + // Run ping for 3 seconds then automatically stop + await runPingWithTimeout('8.8.8.8', 3); +} + +async function demoUserDefinedSignals() { + console.log('\n6. User-Defined Signals (SIGUSR1, SIGUSR2)'); + console.log(' Note: Most commands ignore these signals unless specifically programmed to handle them'); + + try { + const runner = $`sleep 5`; + const promise = runner.start(); + + // Send SIGUSR1 after 1 second + setTimeout(() => { + console.log(' 📡 Sending SIGUSR1 (most processes will ignore this)...'); + runner.kill('SIGUSR1'); + }, 1000); + + // Send SIGUSR2 after 2 seconds + setTimeout(() => { + console.log(' 📡 Sending SIGUSR2 (most processes will ignore this)...'); + runner.kill('SIGUSR2'); + }, 2000); + + // Finally send SIGINT after 3 seconds to actually terminate + setTimeout(() => { + console.log(' 📡 Sending SIGINT to actually terminate...'); + runner.kill('SIGINT'); + }, 3000); + + const result = await promise; + console.log(' ✓ Final exit code:', result.code); + } catch (error) { + console.log(' ✓ Command terminated with exit code:', error.code); + } +} + +// Run all demonstrations +async function main() { + await demoSignalTypes(); + await demoGracefulShutdown(); + await demoInteractiveCommandTermination(); + await demoUserDefinedSignals(); + + console.log('\n🎉 Signal handling demonstration completed!'); + console.log('\nKey takeaways:'); + console.log('• SIGINT (CTRL+C) → Exit code 130'); + console.log('• SIGTERM (default kill) → Exit code 143'); + console.log('• SIGKILL (force kill) → Exit code 137'); + console.log('• Use graceful shutdown pattern: SIGTERM → wait → SIGKILL'); + console.log('• Interactive commands like ping need signals, not stdin'); + console.log('• User signals (SIGUSR1, SIGUSR2) are ignored by most processes'); +} + +main().catch(console.error); \ No newline at end of file diff --git a/examples/sigterm-sigkill-escalation.mjs b/examples/sigterm-sigkill-escalation.mjs new file mode 100644 index 0000000..66bb6db --- /dev/null +++ b/examples/sigterm-sigkill-escalation.mjs @@ -0,0 +1,188 @@ +#!/usr/bin/env node + +/** + * SIGTERM → SIGKILL Escalation Pattern + * + * Demonstrates the proper pattern for graceful shutdown with escalation: + * 1. Send SIGTERM (graceful termination request) + * 2. Wait for process to exit gracefully + * 3. If timeout exceeded, send SIGKILL (force termination) + * + * Usage: + * node examples/sigterm-sigkill-escalation.mjs + */ + +import { $ } from '../src/$.mjs'; + +console.log('📡 SIGTERM → SIGKILL Escalation Demo\n'); + +/** + * Graceful shutdown with escalation + * @param {ProcessRunner} runner - The command runner to shutdown + * @param {number} timeoutMs - Timeout in milliseconds before escalating to SIGKILL + * @returns {Promise} Resolves with the result or error + */ +async function gracefulShutdownWithEscalation(runner, timeoutMs = 5000) { + console.log('🔄 Starting graceful shutdown sequence...'); + + // Step 1: Send SIGTERM (polite termination request) + console.log('📤 Step 1: Sending SIGTERM (graceful termination request)'); + runner.kill('SIGTERM'); + + // Step 2: Set up timeout for escalation to SIGKILL + let escalationTimeout; + const escalationPromise = new Promise((resolve) => { + escalationTimeout = setTimeout(() => { + console.log(`⏰ Step 2: Timeout (${timeoutMs}ms) exceeded, escalating to SIGKILL`); + runner.kill('SIGKILL'); // Force termination + resolve('escalated'); + }, timeoutMs); + }); + + // Step 3: Race between graceful exit and escalation timeout + try { + const result = await Promise.race([ + runner, // Wait for process to exit + escalationPromise // Wait for escalation timeout + ]); + + if (result === 'escalated') { + // Escalation timeout triggered, now wait for SIGKILL to take effect + console.log('🔪 SIGKILL sent, waiting for process termination...'); + const finalResult = await runner; + console.log('✓ Process force-terminated with exit code:', finalResult.code); + return finalResult; + } else { + // Process exited gracefully before timeout + clearTimeout(escalationTimeout); + console.log('✅ Process exited gracefully with exit code:', result.code); + return result; + } + } catch (error) { + clearTimeout(escalationTimeout); + console.log('✓ Process terminated with exit code:', error.code); + return error; + } +} + +/** + * Simulate different process behaviors for testing escalation + */ +async function testEscalationScenarios() { + console.log('Testing different escalation scenarios:\n'); + + // Scenario 1: Process exits gracefully (within timeout) + console.log('📋 Scenario 1: Process that exits quickly (graceful)'); + const runner1 = $`sleep 1`; // Short sleep - will exit before timeout + runner1.start(); + await gracefulShutdownWithEscalation(runner1, 3000); // 3 second timeout + + console.log('\n' + '─'.repeat(50) + '\n'); + + // Scenario 2: Process requires escalation (exceeds timeout) + console.log('📋 Scenario 2: Process that requires SIGKILL (escalation needed)'); + const runner2 = $`sleep 10`; // Long sleep - will exceed timeout + runner2.start(); + // Give it a moment to start then try shutdown with short timeout + setTimeout(() => { + gracefulShutdownWithEscalation(runner2, 2000); // 2 second timeout - will escalate + }, 500); + + // Wait a bit for the escalation demo to complete + await new Promise(resolve => setTimeout(resolve, 4000)); +} + +/** + * Production-ready graceful shutdown function + */ +function createGracefulShutdown(options = {}) { + const { + sigterm_timeout = 5000, // Time to wait for SIGTERM before SIGKILL + sigkill_timeout = 2000, // Time to wait for SIGKILL before giving up + verbose = true + } = options; + + return async function shutdown(runner, reason = 'shutdown requested') { + if (verbose) console.log(`🛑 Graceful shutdown initiated: ${reason}`); + + // Phase 1: SIGTERM + if (verbose) console.log('📤 Phase 1: Sending SIGTERM...'); + runner.kill('SIGTERM'); + + // Phase 2: Wait for graceful exit or timeout + const phase1Promise = new Promise((resolve) => { + setTimeout(() => resolve('timeout'), sigterm_timeout); + }); + + try { + const result = await Promise.race([runner, phase1Promise]); + + if (result === 'timeout') { + // Phase 3: SIGKILL escalation + if (verbose) console.log(`⏰ Phase 2: SIGTERM timeout (${sigterm_timeout}ms), sending SIGKILL...`); + runner.kill('SIGKILL'); + + // Phase 4: Wait for SIGKILL or final timeout + const phase3Promise = new Promise((resolve) => { + setTimeout(() => resolve('final_timeout'), sigkill_timeout); + }); + + const finalResult = await Promise.race([runner, phase3Promise]); + + if (finalResult === 'final_timeout') { + if (verbose) console.log('❌ Final timeout: Process may be hung (this should not happen with SIGKILL)'); + throw new Error('Process termination failed even with SIGKILL'); + } else { + if (verbose) console.log('✓ Process terminated with SIGKILL, exit code:', finalResult.code); + return finalResult; + } + } else { + if (verbose) console.log('✅ Process exited gracefully, exit code:', result.code); + return result; + } + } catch (error) { + if (verbose) console.log('✓ Process terminated, exit code:', error.code); + return error; + } + }; +} + +async function testProductionShutdown() { + console.log('\n' + '='.repeat(60)); + console.log('🏭 Production-Ready Shutdown Function Test\n'); + + // Create shutdown function with custom options + const shutdown = createGracefulShutdown({ + sigterm_timeout: 3000, // 3 seconds for graceful exit + sigkill_timeout: 1000, // 1 second for SIGKILL to take effect + verbose: true + }); + + console.log('📋 Testing production shutdown with long-running process'); + const runner = $`sleep 20`; // Long-running process + runner.start(); + + // Wait a moment then shutdown + setTimeout(() => { + shutdown(runner, 'application shutdown'); + }, 1000); + + // Wait for completion + await new Promise(resolve => setTimeout(resolve, 6000)); +} + +// Run all tests +async function main() { + await testEscalationScenarios(); + await testProductionShutdown(); + + console.log('\n🎉 SIGTERM → SIGKILL Escalation Demo completed!'); + console.log('\nBest Practices:'); + console.log('• Always try SIGTERM first (graceful)'); + console.log('• Set reasonable timeouts (5-30 seconds typical)'); + console.log('• Escalate to SIGKILL if SIGTERM timeout exceeded'); + console.log('• SIGKILL cannot be ignored - it always works'); + console.log('• Monitor exit codes: 143 (SIGTERM), 137 (SIGKILL)'); +} + +main().catch(console.error); \ No newline at end of file