# CTRL-C Handling Implementation Notes ## Overview Implemented graceful CTRL-C handling for sudo password prompts during server creation/generation operations. ## Problem Statement When `fix_local_hosts: true` is set, the provisioning tool requires sudo access to modify `/etc/hosts` and SSH config. When a user cancels the sudo password prompt (no password, wrong password, timeout), the system would: 1. Exit with code 1 (sudo failed) 2. Propagate null values up the call stack 3. Show cryptic Nushell errors about pipeline failures 4. Leave the operation in an inconsistent state **Important Unix Limitation**: Pressing CTRL-C at the sudo password prompt sends SIGINT to the entire process group, interrupting Nushell before exit code handling can occur. This **cannot be caught** and is expected Unix behavior. ## Solution Architecture ### Key Principle: Return Values, Not Exit Codes Instead of using `exit 130` which kills the entire process, we use **return values** to signal cancellation and let each layer of the call stack handle it gracefully. ### Three-Layer Approach 1. **Detection Layer** (ssh.nu helper functions) - Detects sudo cancellation via exit code + stderr - Returns `false` instead of calling `exit` 2. **Propagation Layer** (ssh.nu core functions) - `on_server_ssh()`: Returns `false` on cancellation - `server_ssh()`: Uses `reduce` to propagate failures 3. **Handling Layer** (create.nu, generate.nu) - Checks return values - Displays user-friendly messages - Returns `false` to caller ## Implementation Details ### 1. Helper Functions (ssh.nu:11-32) ```nushell def check_sudo_cached []: nothing -> bool { let result = (do --ignore-errors { ^sudo -n true } | complete) $result.exit_code == 0 } def run_sudo_with_interrupt_check [ command: closure operation_name: string ]: nothing -> bool { let result = (do --ignore-errors { do $command } | complete) if $result.exit_code == 1 and ($result.stderr | str contains "password is required") { print "\n⚠ Operation cancelled - sudo password required but not provided" print "ℹ Run 'sudo -v' first to cache credentials, or run without --fix-local-hosts" return false # Signal cancellation } else if $result.exit_code != 0 and $result.exit_code != 1 { error make {msg: $"($operation_name) failed: ($result.stderr)"} } true } ``` **Design Decision**: Return `bool` instead of throwing error or calling `exit`. This allows the caller to decide how to handle cancellation. ### 2. Pre-emptive Warning (ssh.nu:155-160) ```nushell if $server.fix_local_hosts and not (check_sudo_cached) { print "\n⚠ Sudo access required for --fix-local-hosts" print "ℹ You will be prompted for your password, or press CTRL-C to cancel" print " Tip: Run 'sudo -v' beforehand to cache credentials\n" } ``` **Design Decision**: Warn users upfront so they're not surprised by the password prompt. ### 3. CTRL-C Detection (ssh.nu:171-199) All sudo commands wrapped with detection: ```nushell let result = (do --ignore-errors { ^sudo } | complete) if $result.exit_code == 1 and ($result.stderr | str contains "password is required") { print "\n⚠ Operation cancelled" return false } ``` **Design Decision**: Use `do --ignore-errors` + `complete` to capture both exit code and stderr without throwing exceptions. ### 4. State Accumulation Pattern (ssh.nu:122-129) Using Nushell's `reduce` instead of mutable variables: ```nushell let all_succeeded = ($settings.data.servers | reduce -f true { |server, acc| if $text_match == null or $server.hostname == $text_match { let result = (on_server_ssh $settings $server $ip_type $request_from $run) $acc and $result } else { $acc } }) ``` **Design Decision**: Nushell doesn't allow mutable variable capture in closures. Use `reduce` for accumulating boolean state across iterations. ### 5. Caller Handling (create.nu:262-266, generate.nu:269-273) ```nushell let ssh_result = (on_server_ssh $settings $server "pub" "create" false) if not $ssh_result { _print "\n✗ Server creation cancelled" return false } ``` **Design Decision**: Check return value and provide context-specific message before returning. ## Error Flow Diagram ``` User presses CTRL-C during password prompt ↓ sudo exits with code 1, stderr: "password is required" ↓ do --ignore-errors captures exit code & stderr ↓ Detection logic identifies cancellation ↓ Print user-friendly message ↓ Return false (not exit!) ↓ on_server_ssh returns false ↓ Caller (create.nu/generate.nu) checks return value ↓ Print "✗ Server creation cancelled" ↓ Return false to settings.nu ↓ settings.nu handles false gracefully (no append) ↓ Clean exit, no cryptic errors ``` ## Nushell Idioms Used ### 1. `do --ignore-errors` + `complete` Captures both stdout, stderr, and exit code without throwing: ```nushell let result = (do --ignore-errors { ^sudo command } | complete) # result = { stdout: "...", stderr: "...", exit_code: 1 } ``` ### 2. `reduce` for Accumulation Instead of mutable variables in loops: ```nushell # ❌ BAD - mutable capture in closure mut all_succeeded = true $servers | each { |s| $all_succeeded = false # Error: capture of mutable variable } # ✅ GOOD - reduce with accumulator let all_succeeded = ($servers | reduce -f true { |s, acc| $acc and (check_server $s) }) ``` ### 3. Early Returns for Error Handling ```nushell if not $condition { print "Error message" return false } # Continue with happy path ``` ## Testing Scenarios ### Scenario 1: CTRL-C During First Sudo Command ```bash provisioning -c server create # Password: [CTRL-C] # Expected Output: # ⚠ Operation cancelled - sudo password required but not provided # ℹ Run 'sudo -v' first to cache credentials # ✗ Server creation cancelled ``` ### Scenario 2: Pre-cached Credentials ```bash sudo -v provisioning -c server create # Expected: No password prompt, smooth operation ``` ### Scenario 3: Wrong Password 3 Times ```bash provisioning -c server create # Password: [wrong] # Password: [wrong] # Password: [wrong] # Expected: Same as CTRL-C (treated as cancellation) ``` ### Scenario 4: Multiple Servers, Cancel on Second ```bash # If creating multiple servers and CTRL-C on second: # - First server completes successfully # - Second server shows cancellation message # - Operation stops, doesn't proceed to third ``` ## Maintenance Notes ### Adding New Sudo Commands When adding new sudo commands to the codebase: 1. Wrap with `do --ignore-errors` + `complete` 2. Check for exit code 1 + "password is required" 3. Return `false` on cancellation 4. Let caller handle the `false` return value Example template: ```nushell let result = (do --ignore-errors { ^sudo new-command } | complete) if $result.exit_code == 1 and ($result.stderr | str contains "password is required") { print "\n⚠ Operation cancelled - sudo password required" return false } ``` ### Common Pitfalls 1. **Don't use `exit`**: It kills the entire process 2. **Don't use mutable variables in closures**: Use `reduce` instead 3. **Don't ignore return values**: Always check and propagate 4. **Don't forget the pre-check warning**: Users should know sudo is needed ## Future Improvements 1. **Sudo Credential Manager**: Optionally use a credential manager (keychain, etc.) 2. **Sudo-less Mode**: Alternative implementation that doesn't require root 3. **Timeout Handling**: Detect when sudo times out waiting for password 4. **Multiple Password Attempts**: Distinguish between CTRL-C and wrong password ## References - Nushell `complete` command: https://www.nushell.sh/commands/docs/complete.html - Nushell `reduce` command: https://www.nushell.sh/commands/docs/reduce.html - Sudo exit codes: man sudo (exit code 1 = authentication failure) - POSIX signal conventions: SIGINT (CTRL-C) = 130 ## Related Files - `provisioning/core/nulib/servers/ssh.nu` - Core implementation - `provisioning/core/nulib/servers/create.nu` - Calls on_server_ssh - `provisioning/core/nulib/servers/generate.nu` - Calls on_server_ssh - `docs/troubleshooting/CTRL-C_SUDO_HANDLING.md` - User-facing docs - `docs/quick-reference/SUDO_PASSWORD_HANDLING.md` - Quick reference ## Changelog - **2025-01-XX**: Initial implementation with return values (v2) - **2025-01-XX**: Fixed mutable variable capture with `reduce` pattern - **2025-01-XX**: First attempt with `exit 130` (reverted, caused process termination)