Skip to content

一、为什么在 Jenkins 中需要 Expect? ​

在传统的 Linux 运维与部署脚本中,很多命令或自研运维二进制工具会强制要求交互式输入,例如:

  • 目标机器首次建立 SSH 信任时的 Are you sure you want to continue connecting (yes/no)? 确认提示;
  • 特定遗留系统必须通过交互式标准输入读取的动态安全口令;
  • 自定义安装程序运行期间要求人工确认的 [Y/N] 交互分支。

当这些脚本被放入 Jenkins CI/CD 自动化流水线(非交互式无 TTY 会话)中运行时,进程会因为等待用户在控制台键入回车而陷入无限期的挂死(Hang)状态,直到触发构建超时强行终止。

通过将 Expect 状态机 集成到 Jenkins Pipeline 中,可以由程序自动监听终端输出的预期正则并精准模拟键盘输入,实现 100% 自动化无人值守部署。


二、Expect 核心语法与执行模型 ​

Expect 运行在 Tcl 解释器之上,其核心模型由四大命令驱动:

  1. spawn:派生并启动一个交互式子进程(如 ssh、scp、passwd);
  2. expect:从进程的标准输出中模式匹配关键词(支持超时配置);
  3. send:向子进程发送模拟的键盘键入字符串(需以 \r 回车结尾);
  4. interact / expect eof:将控制权交还用户或等待子进程正常执行完毕退出。

三、Jenkins Pipeline 动态下发 Expect 脚本模式 ​

在 Jenkinsfile 中,通常推荐使用 writeFile 在构建工作区动态生成临时 Expect 脚本,再通过 sh 步骤拉起执行:

groovy
def executeInteractiveDeploy(String targetHost, String commandToRun) {
    // 从 Jenkins 安全凭据中提取临时访问口令
    withCredentials([usernamePassword(credentialsId: 'cluster-deploy-token',
                                      usernameVariable: 'SSH_USER',
                                      passwordVariable: 'SSH_PASS')]) {
        
        // 动态生成 Expect 脚本,使用三单引号避免 Groovy 变量插值混乱
        writeFile file: 'deploy_runner.exp', text: """#!/usr/bin/expect -f
            set timeout 120
            set host "${targetHost}"
            set user "${SSH_USER}"
            set password "${SSH_PASS}"
            set cmd "${commandToRun}"

            # 启动 SSH 子进程
            spawn ssh -o PreferredAuthentications=password,keyboard-interactive \$user@\$host \$cmd

            # 状态机多分支匹配
            expect {
                -nocase "yes/no" {
                    send "yes\\r"
                    exp_continue
                }
                -nocase "password:" {
                    send "\$password\\r"
                    exp_continue
                }
                timeout {
                    puts "\\n[ERROR] 连接超时或未收到预期的提示符!\\n"
                    exit 1
                }
                eof {
                    puts "\\n[INFO] 远程指令执行完毕正常退出。\\n"
                }
            }

            # 捕获远程子进程的实际退出码
            catch wait result
            set exit_code [lindex \$result 3]
            exit \$exit_code
        """

        // 赋予执行权限并调用执行
        sh """
            chmod +x deploy_runner.exp
            ./deploy_runner.exp
            rm -f deploy_runner.exp
        """
    }
}

四、生产避坑要点与防御性设计 ​

1. 无 TTY 与终端环境变量问题 ​

Jenkins 的执行 Agent 默认不分配虚拟终端伪设备(PTY),某些 Expect 版本在无 TTY 时可能会直接报错 spawn id not open。

  • 解法:在 Jenkinsfile 的 sh 之前显式导出 export TERM=xterm,或在 spawn 参数中使用 -noecho。

2. exp_continue 的正确使用场景 ​

当同一个进程可能连续触发多个交互提示(例如:首次连接先询问 yes/no,接着再询问 password:)时,必须在对应的匹配分支内加上 exp_continue,指示 Expect 状态机继续驻留并等待下一条匹配规则,否则匹配到第一条后就会提前跳出。

3. 超时时间(timeout)合理设定 ​

Expect 默认的单步超时时间较短(通常为 10 秒)。如果远程执行的是耗时较长的解压、大文件拉取或编译操作,必须在脚本开头显式提高超时时间(如 set timeout 300),避免因大文件传输耗时过久被 Expect 误判为会话僵死。

测试开发工程师 · 专注自动化与系统架构 | 邮箱: hansblog@atumsoul.win