provisioning/docs/src/development/ctrl-c-implementation-notes.md
2026-01-14 04:59:11 +00:00

295 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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)
```python
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 "
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 "
⚠ 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
"
}
```
**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:
```javascript
let result = (do --ignore-errors { ^sudo <command> } | complete)
if $result.exit_code == 1 and ($result.stderr | str contains "password is required") {
print "
⚠ 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:
```javascript
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)
```javascript
let ssh_result = (on_server_ssh $settings $server "pub" "create" false)
if not $ssh_result {
_print "
✗ Server creation cancelled"
return false
}
```
**Design Decision**: Check return value and provide context-specific message before returning.
## Error Flow Diagram
```bash
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:
```javascript
let result = (do --ignore-errors { ^sudo command } | complete)
# result = { stdout: "...", stderr: "...", exit_code: 1 }
```
### 2. `reduce` for Accumulation
Instead of mutable variables in loops:
```bash
# ❌ 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
```bash
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:
```javascript
let result = (do --ignore-errors { ^sudo new-command } | complete)
if $result.exit_code == 1 and ($result.stderr | str contains "password is required") {
print "
⚠ 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)