SSH认证进程复用问题:Expect无法捕获输出导致超时
Let's break down what's going wrong with your current approach and fix it with two practical solutions—one using Expect with named pipes, and a better alternative leveraging SSH's built-in session reuse feature.
Why Your Current Setup Fails
Your ssh_cmd function pipes ssh output to an Expect instance that handles the password prompt. The core issues are:
- Once that first Expect instance finishes sending the password and exits, it closes the pipe connected to
ssh's stdout. This causes the SSH process to terminate immediately, so there's no active session left for your second Expect script to interact with. - Pipes are one-way and designed for one-off data transfer, not ongoing interactive communication—they can't maintain a persistent session.
Solution 1: Persistent SSH Session with Expect & Named Pipes
We'll use named pipes (FIFOs) to create a bidirectional channel between your shell and the persistent SSH session managed by Expect. This keeps the SSH process alive so you can send commands later.
Add these functions to your shell profile:
# Start a persistent SSH session with password auth start_ssh_session() { # Define unique pipe paths for the session local SSH_IN="/tmp/ssh_in_${USER}_${HOST}" local SSH_OUT="/tmp/ssh_out_${USER}_${HOST}" local PID_FILE="/tmp/ssh_pid_${USER}_${HOST}" # Clean up existing pipes if they exist rm -f "$SSH_IN" "$SSH_OUT" "$PID_FILE" mkfifo "$SSH_IN" "$SSH_OUT" # Launch Expect script in background to manage the SSH session expect -c " spawn ssh ${USER}@${HOST} set timeout 5 # Handle password prompt expect \"password:\" { send \"${PASS}\n\" } # Wait for the shell prompt to confirm connection success expect -re {\$|#} # Forward SSH output to the output pipe fileevent stdout readable { set data [read stdout] puts -nonewline \"$SSH_OUT\" \$data } # Read commands from the input pipe and send to SSH while {1} { if {[catch {read \"$SSH_IN\"} cmd]} { break } send \$cmd } # Cleanup on exit close \"$SSH_IN\" close \"$SSH_OUT\" file delete \"$SSH_IN\" file delete \"$SSH_OUT\" } & # Save the Expect process ID for cleanup echo \$! > \"$PID_FILE\" echo \"SSH session started (PID: \$(cat $PID_FILE))\" } # Send a command to the persistent SSH session ssh_send_cmd() { local SSH_IN="/tmp/ssh_in_${USER}_${HOST}" local SSH_OUT="/tmp/ssh_out_${USER}_${HOST}" local cmd="$1\n" # Send the command to the input pipe echo -ne "$cmd" > "$SSH_IN" # Read output until we get the prompt again (adjust regex to match your shell prompt) awk '/#/{print; exit}' "$SSH_OUT" } # Stop the persistent SSH session stop_ssh_session() { local PID_FILE="/tmp/ssh_pid_${USER}_${HOST}" local SSH_IN="/tmp/ssh_in_${USER}_${HOST}" local SSH_OUT="/tmp/ssh_out_${USER}_${HOST}" if [ -f "$PID_FILE" ]; then kill "$(cat "$PID_FILE")" rm -f "$PID_FILE" "$SSH_IN" "$SSH_OUT" echo "SSH session stopped" fi }
Usage Example:
# Start the session HOST="ui1" USER="ubnt" PASS="your_password" start_ssh_session # Run commands ssh_send_cmd "pwd" ssh_send_cmd "ls -l" # Cleanup when done stop_ssh_session
Solution 2: Use SSH's Built-in ControlMaster (Recommended)
SSH has a native feature called ControlMaster that lets you reuse an existing SSH connection for multiple commands—no need to manage Expect sessions manually. We'll only use Expect once to authenticate the initial master connection.
Add these functions:
# Initialize a reusable SSH session with password auth ssh_init_session() { local CONTROL_SOCKET="/tmp/ssh_control_${USER}_${HOST}" # Clean up existing socket if present rm -f "$CONTROL_SOCKET" # Use Expect to establish the master connection (backgrounded) expect -c " spawn ssh -o ControlMaster=yes -o ControlPath=$CONTROL_SOCKET -o ControlPersist=yes ${USER}@${HOST} set timeout 5 expect \"password:\" { send \"${PASS}\n\" } # Wait for prompt to confirm connection expect -re {\$|#} # Send a dummy command to keep the session alive briefly send \"echo Session ready\n\" " & # Wait a few seconds for the session to be fully established sleep 3 echo "Reusable SSH session initialized (socket: $CONTROL_SOCKET)" } # Run a command using the reusable session ssh_run_cmd() { local CONTROL_SOCKET="/tmp/ssh_control_${USER}_${HOST}" ssh -o ControlPath="$CONTROL_SOCKET" "${USER}@${HOST}" "$1" } # Close the reusable SSH session ssh_close_session() { local CONTROL_SOCKET="/tmp/ssh_control_${USER}_${HOST}" ssh -o ControlPath="$CONTROL_SOCKET" -O exit "${USER}@${HOST}" rm -f "$CONTROL_SOCKET" echo "Reusable SSH session closed" }
Usage Example:
# Initialize the session HOST="ui1" USER="ubnt" PASS="your_password" ssh_init_session # Run commands without re-authenticating ssh_run_cmd "pwd" ssh_run_cmd "sudo apt update" # Close the session when finished ssh_close_session
Why ControlMaster is Better
- It's a native SSH feature, so it's more stable and maintainable than rolling your own Expect-based session manager.
- You can use regular
sshcommands (or evenscp/sftp) to reuse the session—no custom functions needed after initialization. - It handles session persistence and cleanup more reliably out of the box.
内容的提问来源于stack exchange,提问作者PoVa

