2025-12-11 21:50:42 +00:00
# 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
}
2026-01-08 09:55:37 +00:00
```plaintext
2025-12-11 21:50:42 +00:00
**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"
}
2026-01-08 09:55:37 +00:00
```plaintext
2025-12-11 21:50:42 +00:00
**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
}
2026-01-08 09:55:37 +00:00
```plaintext
2025-12-11 21:50:42 +00:00
**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
}
})
2026-01-08 09:55:37 +00:00
```plaintext
2025-12-11 21:50:42 +00:00
**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
}
2026-01-08 09:55:37 +00:00
```plaintext
2025-12-11 21:50:42 +00:00
**Design Decision**: Check return value and provide context-specific message before returning.
## Error Flow Diagram
2026-01-08 09:55:37 +00:00
```plaintext
2025-12-11 21:50:42 +00:00
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
2026-01-08 09:55:37 +00:00
```plaintext
2025-12-11 21:50:42 +00:00
## 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 }
2026-01-08 09:55:37 +00:00
```plaintext
2025-12-11 21:50:42 +00:00
### 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)
})
2026-01-08 09:55:37 +00:00
```plaintext
2025-12-11 21:50:42 +00:00
### 3. Early Returns for Error Handling
```nushell
if not $condition {
print "Error message"
return false
}
# Continue with happy path
2026-01-08 09:55:37 +00:00
```plaintext
2025-12-11 21:50:42 +00:00
## 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
2026-01-08 09:55:37 +00:00
```plaintext
2025-12-11 21:50:42 +00:00
### Scenario 2: Pre-cached Credentials
```bash
sudo -v
provisioning -c server create
# Expected: No password prompt, smooth operation
2026-01-08 09:55:37 +00:00
```plaintext
2025-12-11 21:50:42 +00:00
### 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)
2026-01-08 09:55:37 +00:00
```plaintext
2025-12-11 21:50:42 +00:00
### 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
2026-01-08 09:55:37 +00:00
```plaintext
2025-12-11 21:50:42 +00:00
## 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
}
2026-01-08 09:55:37 +00:00
```plaintext
2025-12-11 21:50:42 +00:00
### 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
2026-01-08 09:55:37 +00:00
- Nushell `complete` command: < https: // www . nushell . sh / commands / docs / complete . html >
- Nushell `reduce` command: < https: // www . nushell . sh / commands / docs / reduce . html >
2025-12-11 21:50:42 +00:00
- 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)