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:
- Exit with code 1 (sudo failed)
- Propagate null values up the call stack
- Show cryptic Nushell errors about pipeline failures
- 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
-
Detection Layer (ssh.nu helper functions)
- Detects sudo cancellation via exit code + stderr
- Returns
falseinstead of callingexit
-
Propagation Layer (ssh.nu core functions)
on_server_ssh(): Returnsfalseon cancellationserver_ssh(): Usesreduceto propagate failures
-
Handling Layer (create.nu, generate.nu)
- Checks return values
- Displays user-friendly messages
- Returns
falseto caller
Implementation Details
1. Helper Functions (ssh.nu:11-32)
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)
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:
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:
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)
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:
let result = (do --ignore-errors { ^sudo command } | complete)
# result = { stdout: "...", stderr: "...", exit_code: 1 }
2. reduce for Accumulation
Instead of mutable variables in loops:
# ❌ 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
if not $condition {
print "Error message"
return false
}
# Continue with happy path
Testing Scenarios
Scenario 1: CTRL-C During First Sudo Command
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
sudo -v
provisioning -c server create
# Expected: No password prompt, smooth operation
Scenario 3: Wrong Password 3 Times
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
# 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:
- Wrap with
do --ignore-errors+complete - Check for exit code 1 + “password is required”
- Return
falseon cancellation - Let caller handle the
falsereturn value
Example template:
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
- Don’t use
exit: It kills the entire process - Don’t use mutable variables in closures: Use
reduceinstead - Don’t ignore return values: Always check and propagate
- Don’t forget the pre-check warning: Users should know sudo is needed
Future Improvements
- Sudo Credential Manager: Optionally use a credential manager (keychain, etc.)
- Sudo-less Mode: Alternative implementation that doesn’t require root
- Timeout Handling: Detect when sudo times out waiting for password
- Multiple Password Attempts: Distinguish between CTRL-C and wrong password
References
- Nushell
completecommand: https://www.nushell.sh/commands/docs/complete.html - Nushell
reducecommand: 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 implementationprovisioning/core/nulib/servers/create.nu- Calls on_server_sshprovisioning/core/nulib/servers/generate.nu- Calls on_server_sshdocs/troubleshooting/CTRL-C_SUDO_HANDLING.md- User-facing docsdocs/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
reducepattern - 2025-01-XX: First attempt with
exit 130(reverted, caused process termination)