一、为什么在 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 解释器之上,其核心模型由四大命令驱动:
spawn:派生并启动一个交互式子进程(如ssh、scp、passwd);expect:从进程的标准输出中模式匹配关键词(支持超时配置);send:向子进程发送模拟的键盘键入字符串(需以\r回车结尾);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 误判为会话僵死。