provisioning/docs/src/development/ctrl-c-implementation-notes.md

2 lines
8.5 KiB
Markdown
Raw Normal View History

# CTRL-C Handling Implementation Notes\n\n## Overview\n\nImplemented graceful CTRL-C handling for sudo password prompts during server creation/generation operations.\n\n## Problem Statement\n\nWhen `fix_local_hosts: true` is set, the provisioning tool requires sudo access to\nmodify `/etc/hosts` and SSH config. When a user cancels the sudo password prompt (no\npassword, wrong password, timeout), the system would:\n\n1. Exit with code 1 (sudo failed)\n2. Propagate null values up the call stack\n3. Show cryptic Nushell errors about pipeline failures\n4. Leave the operation in an inconsistent state\n\n**Important Unix Limitation**: Pressing CTRL-C at the sudo password prompt sends SIGINT to the entire process group, interrupting Nushell before exit\ncode handling can occur. This **cannot be caught** and is expected Unix behavior.\n\n## Solution Architecture\n\n### Key Principle: Return Values, Not Exit Codes\n\nInstead of using `exit 130` which kills the entire process, we use **return values**\nto signal cancellation and let each layer of the call stack handle it gracefully.\n\n### Three-Layer Approach\n\n1. **Detection Layer** (ssh.nu helper functions)\n - Detects sudo cancellation via exit code + stderr\n - Returns `false` instead of calling `exit`\n\n2. **Propagation Layer** (ssh.nu core functions)\n - `on_server_ssh()`: Returns `false` on cancellation\n - `server_ssh()`: Uses `reduce` to propagate failures\n\n3. **Handling Layer** (create.nu, generate.nu)\n - Checks return values\n - Displays user-friendly messages\n - Returns `false` to caller\n\n## Implementation Details\n\n### 1. Helper Functions (ssh.nu:11-32)\n\n```\ndef check_sudo_cached []: nothing -> bool {\n let result = (do --ignore-errors { ^sudo -n true } | complete)\n $result.exit_code == 0\n}\n\ndef run_sudo_with_interrupt_check [\n command: closure\n operation_name: string\n]: nothing -> bool {\n let result = (do --ignore-errors { do $command } | complete)\n if $result.exit_code == 1 and ($result.stderr | str contains "password is required") {\n print "\n⚠ Operation cancelled - sudo password required but not provided"\n print " Run 'sudo -v' first to cache credentials, or run without --fix-local-hosts"\n return false # Signal cancellation\n } else if $result.exit_code != 0 and $result.exit_code != 1 {\n error make {msg: $"($operation_name) failed: ($result.stderr)"}\n }\n true\n}\n```\n\n**Design Decision**: Return `bool` instead of throwing error or calling `exit`. This allows the caller to decide how to handle cancellation.\n\n### 2. Pre-emptive Warning (ssh.nu:155-160)\n\n```\nif $server.fix_local_hosts and not (check_sudo_cached) {\n print "\n⚠ Sudo access required for --fix-local-hosts"\n print " You will be prompted for your password, or press CTRL-C to cancel"\n print " Tip: Run 'sudo -v' beforehand to cache credentials\n"\n}\n```\n\n**Design Decision**: Warn users upfront so they're not surprised by the password prompt.\n\n### 3. CTRL-C Detection (ssh.nu:171-199)\n\nAll sudo commands wrapped with detection:\n\n```\nlet result = (do --ignore-errors { ^sudo <command> } | complete)\nif $result.exit_code == 1 and ($result.stderr | str contains "password is required") {\n print "\n⚠ Operation cancelled"\n return false\n}\n```\n\n**Design Decision**: Use `do --ignore-errors` + `complete` to capture both exit code and stderr without throwing exceptions.\n\n### 4. State Accumulation Pattern (ssh.nu:122-129)\n\nUsing Nushell's `reduce` instead of mutable variables:\n\n```\nlet all_succeeded = ($settings.data.servers | reduce -f true { |server, acc|\n if $text_match == null or $server.hostname == $text_match {\n let result = (on_server_ssh $settings $server $ip_type $request_from $run)\n $acc and $result\n } else {\n $acc\n }\n})\n```\n\n**Design Decision**: Nushell doesn't allow mutable variable capture in closures. Use `reduce` for accumulating boolean state across iterations.\n\n### 5. Caller Handling (create.nu:262-266, generate.nu:269-273)\n\n```\nlet ssh_result = (on_server_ssh $setti