provisioning/docs/src/development/CTRL-C_IMPLEMENTATION_NOTES.md

286 lines
8.3 KiB
Markdown
Raw Normal View History

# 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 <command> } | 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)