Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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)

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:

  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:

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
  • 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)