pexpect嵌套管理进程:无法捕获Rails容器输出的问题排查
需要通过三层嵌套控制台执行功能:
- 本地主机:通过
aws ssm start-session ...连接堡垒机实例 - 堡垒机:调用Shell脚本启动并连接Ruby on Rails容器
- Rails容器:执行功能后退出
尝试用pexpect程序化管理该流程,预期用pexpect.spawn启动进程,通过sendline()发送命令,捕获Rails容器的所有stdout后退出,但实际仅能捕获Rails容器启动脚本的输出,无法捕获容器内执行命令(如2+2)的输出,也不确定输入是否送达容器。测试时用p.interact()可正常操作,但最终场景不能使用该方法,用tee管道也无效。
执行代码如下:
aws_command = "aws ssm start-session ..." p = pexpect.spawn(aws_command, encoding='utf-8') logfile = open("logs.txt", 'w') p.logfile = logfile p.expect('$') p.sendline('ls') # ... other setup commands/env vars ... p.sendline('this is where the command for a shell script that starts a ruby on rails container is sent') p.expect('Loading') # this is the final line catpured from the ssm process before the rails console begins time.sleep(30) # extra wait for good measure p.sendline('2+2') # should see an output of 4 in the logs time.sleep(5) p.sendline('exit') p.kill(0)
核心疑问:
- pexpect能否管理此类嵌套进程?
- 是输出未被捕获,还是输入未送达Rails容器?
怀疑需要在SSM实例中注入中间Python脚本,实现输入输出的全管道传输。
1. pexpect完全支持嵌套进程管理
pexpect的核心是监控子进程的STDIN/STDOUT流,只要所有层级的输入输出都通过同一个终端流传递(即堡垒机启动Rails容器时直接附着当前终端,而非后台或独立会话),pexpect就能正常捕获和发送数据。你测试时p.interact()能正常工作,就证明流是通的,问题出在脚本的等待和匹配逻辑上。
2. 问题根源:等待逻辑错误+未匹配Rails控制台提示符
你当前用p.expect('Loading')后直接sleep再发命令,但Rails控制台启动完成后会有专属提示符(比如rails(main):001:0>),没等这个提示符就发命令,输入大概率没送到Rails控制台的输入流;同时,因为没监控这个提示符后的输出,自然捕获不到2+2的结果。另外,固定时长的sleep非常不可靠,网络延迟、容器启动速度波动都会导致时机错位,必须用pexpect的expect匹配明确标识,而非依赖sleep。
具体修正步骤
- 替换sleep为expect匹配Rails控制台提示符:
把time.sleep(30)换成p.expect(r'rails\(main\):\d+:\d>')(根据实际控制台提示符调整正则),确认控制台真正就绪后再发送命令。 - 优化日志配置:可以把
logfile换成logfile_read,只捕获进程输出,避免混入自己发送的命令,方便排查问题。 - 避免过早终止进程:
p.kill(0)可能在Rails容器输出未完全刷新时就终止进程,应该等待容器退出标识,或用p.wait()等待进程自然结束。
修正后的关键代码片段:
# 发送启动Rails容器的脚本命令后,等待控制台就绪 p.sendline('your-rails-container-start-script.sh') p.expect('Loading') # 匹配Rails控制台的提示符,确认就绪 p.expect(r'rails\(main\):\d+:\d>') # 发送计算命令 p.sendline('2+2') # 匹配输出结果或下一个提示符 p.expect(r'4') # 退出Rails控制台 p.sendline('exit') # 等待回到堡垒机提示符 p.expect('$') # 退出SSM会话 p.sendline('exit') p.wait()
中间脚本备选方案
如果堡垒机上启动Rails容器的脚本默认未附着终端(比如用了docker exec -d后台模式),需调整启动方式为docker exec -it your-rails-container rails console(确保用-it参数强制分配伪终端),让容器输入输出继承当前SSM会话的终端流,pexpect就能正常捕获。
如果必须用中间脚本,可在堡垒机上运行一个简单的Python脚本,专门负责启动Rails容器并转发输入输出,但优先调整启动命令和本地pexpect的匹配逻辑更高效。
内容的提问来源于stack exchange,提问作者CatCatMcMeows

