You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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:

  1. 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.
  2. 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

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 ssh commands (or even scp/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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.06 13:52:38