从 asyncio 子进程获取实时输出

时间:2021-01-10 01:42:19

标签: python python-asyncio

我正在尝试使用 Python asyncio 子进程来启动交互式 SSH 会话并自动输入密码。实际用例并不重要,但它有助于说明我的问题。这是我的代码:

    proc = await asyncio.create_subprocess_exec(
        'ssh', 'user@127.0.0.1',
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.STDOUT,
        stdin=asyncio.subprocess.PIPE,
    )

    # This loop could be replaced by async for, I imagine
    while True:
        buf = await proc.stdout.read()
        if not buf:
            break
        print(f'stdout: { buf }')

我希望它能够像 asyncio 流一样工作,我可以在其中创建两个任务/子例程/未来,一个用于侦听 StreamReader(在本例中由 proc.stdout 给出),另一个用于侦听写入 StreamWriter (proc.stdin)。

但是,它没有按预期工作。 ssh 命令的前几行输出直接打印到终端,直到它进入密码提示(或主机密钥提示,视情况而定)并等待手动输入。我希望能够阅读前几行,检查它是否要求输入密码或主机提示,并相应地写入 StreamReader。

它唯一一次运行 print(f'stdout: { buf }') 行是在我按下 Enter 后,当它打印时,显然,“stderr: b'Host key verification failed.\r\n'”。

我也尝试了推荐的 proc.communicate(),它不像使用 StreamReader/Writer 那样简洁,但它有同样的问题:执行在等待手动输入时冻结。

这实际上应该如何工作?如果这不是我想象的那样,为什么不呢,有没有办法在不诉诸线程中的某种繁忙循环的情况下实现这一目标?

PS:为了清楚起见,我正在解释使用 ssh。我最终使用了我想要的 plink,但我想了解如何使用 python 执行此操作以运行任意命令。

2 个答案:

答案 0 :(得分:2)

这不是 asyncio 特有的问题。 ssh 进程不与 stdinstdout 流交互,而是直接访问 TTY device,以确保正确保护密码输入。

您可以通过三个选项来解决此问题:

  • 不要使用 ssh,而是使用其他一些 SSH 客户端,一个不希望 TTY 控制的客户端。对于 asyncio,您可以使用 asyncssh library。该库直接实现了 SSH 协议,因此不需要单独的进程,它直接接受用户名和密码凭据。

  • 为 SSH 提供一个 pseudo-tty 以与之对话,这是您的 Python 程序控制的。 pexpect library 提供了一个高级 API,可为您执行此操作,并可用于完全控制 ssh 命令。

  • 设置备用密码提示器以供 ssh 使用。如果没有 TTY,ssh 程序可以通过 SSH_ASKPASS 环境变量让 something else 处理密码输入。大多数版本的 ssh 都相当 picky about when they'll accept SSH_ASKPASS however,您也需要设置 DISPLAY,使用 -n 命令行开关 ssh and 使用 setsid command 在与任何 TTY 断开连接的新会话中运行 ssh

    我之前已经描述了如何在 answer to a question about git and ssh 中将 SSH_ASKPASS 与 asyncio 结合使用。

阻力最小的路径是使用 pexpect,因为它原生支持 asyncio(任何接受 async_=True 的方法都可以用作协程):

import pexpect

proc = pexpect.spawn('ssh user@127.0.0.1')
await child.expect('password:', timeout=120, async_=True) 
child.sendline(password_for_user)

答案 1 :(得分:1)

此处演示实时输出。
简而言之,运行 bash process -> with stdin pass an 'ls' command -> async read result from the stdout

    proc = await asyncio.create_subprocess_exec(
        '/bin/bash', '-i', 
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.STDOUT,
        stdin=asyncio.subprocess.PIPE,
    )

    proc.stdin.write(b'ls \r\n')
    await proc.stdin.drain()  
    
    try:
        while True:
            # wait line for 3 seconds or raise an error
            line = await asyncio.wait_for( proc.stdout.readline(), 3 )\
            print(line)
    except asyncio.TimeoutError:
        pass


使用这种技术,我无法使用 ssh 和“密码”进入服务器,
我在命令 'ssh -tt user@localhost '

之后出现错误“bash: no job control in this shell”