subprocess.Popen管控SSH进程时管道IO意外阻塞问题求助
搞定SSH交互进程的subprocess用法问题
嘿,我看你在用subprocess控制SSH时,不加-tt能跑,但加了之后就出问题——其实核心坑点在你循环调用communicate()的方式上,这玩意儿根本不是这么用的!
先给你掰扯清楚:communicate()是一次性把所有输入喂给进程,然后等着进程结束,一次性读取所有输出。你把它放进while循环反复调用,尤其是加了-tt之后,SSH会保持会话开着等你交互,communicate()超时后自然就乱套了。
不加-tt时看起来“正常”,其实是因为SSH没分配伪终端,登录后可能默认shell没任务就直接退出了,communicate()刚好能读完输出,但这根本没法支持你要的远程交互程序(比如vim、top这类需要终端的玩意儿)。
两种正确的交互姿势
姿势1:半自动化交互(发送特定命令)
如果你只是要执行几个固定的交互步骤(比如输密码、启动某个程序、执行操作),可以用分步写stdin+监听stdout的方式,别再用communicate()了。给你整个示例:
import subprocess import select # 启动带伪终端的SSH进程 p = subprocess.Popen( ['ssh', '-tt', 'LOGIN@HOSTNAME'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, # 把错误输出合并到标准输出,省得分开处理 universal_newlines=True, bufsize=1 # 行缓冲,能实时看到输出 ) # 实时读取输出的小工具函数 def read_ssh_output(): while True: # 用select监听有没有输出可读,超时0.5秒避免卡死 ready, _, _ = select.select([p.stdout], [], [], 0.5) if ready: line = p.stdout.readline() if not line: break print(line.strip()) # 检查进程是不是已经挂了 if p.poll() is not None: break # 第一步:等登录提示,发密码(如果需要的话) read_ssh_output() p.stdin.write("你的SSH密码\n") p.stdin.flush() # 一定要flush,不然内容会留在缓冲区发不出去 # 第二步:发命令启动交互程序,比如vim read_ssh_output() p.stdin.write("vim test.txt\n") p.stdin.flush() # 第三步:模拟vim操作——按i进插入模式,输入内容,按Esc,存盘退出 read_ssh_output() p.stdin.write("i从subprocess发来的消息!\n") p.stdin.flush() read_ssh_output() p.stdin.write("\x1b") # 这是Esc键的ASCII码 p.stdin.flush() read_ssh_output() p.stdin.write(":wq\n") p.stdin.flush() # 最后收尾,读完剩余输出,等进程结束 read_ssh_output() p.wait()
姿势2:完全实时交互(像手动操作SSH一样)
要是你想完全模拟手动敲SSH的体验,随时输入命令随时看输出,那用pty模块配合subprocess更靠谱,它能创建伪终端,完美模拟真实终端环境:
import subprocess import pty import os # 创建伪终端的主从端 master, slave = pty.openpty() # 启动SSH,把所有IO绑定到伪终端(这里不用加-tt,pty已经搞定终端了) p = subprocess.Popen( ['ssh', 'LOGIN@HOSTNAME'], stdin=slave, stdout=slave, stderr=slave, universal_newlines=True ) os.close(slave) # 不用的一端关掉 # 实时交互循环 while True: try: # 读SSH输出 output = os.read(master, 1024).decode() if not output: break print(output, end='') # 可以加自动判断逻辑,比如看到密码提示就自动发密码 if "Password:" in output: os.write(master, b"你的SSH密码\n") # 看到命令提示符,就让用户输入命令 elif "$ " in output or "# " in output: user_cmd = input("") + "\n" os.write(master, user_cmd.encode()) except OSError: # 进程结束时会抛OSError,直接退出循环 break p.wait() os.close(master)
最后划几个重点
communicate()只能用一次!一次!别循环调用,它是为一次性执行完就结束的进程设计的。-tt的作用是强制SSH分配伪终端,这是运行远程交互程序的必要条件,但会让SSH保持会话打开,不会自动退出。- 处理实时交互时,要么用
select监听输出流,要么用pty创建伪终端,这俩才是正确打开方式。
内容的提问来源于stack exchange,提问作者Octo Fish
相关产品推荐
相关产品推荐

