You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

启用stderr时,subprocess启动后台SSH进程的communicate挂起问题

问题:SSH控制套接字启动时subprocess.communicate()挂起问题

需求

通过Python的subprocess实现两个功能:

  • 执行ssh -f -M在后台启动共享套接字
  • 后续SSH连接复用/tmp/control-channel套接字实现免密登录

测试代码(test.py)

import subprocess
import os
import sys
import stat

ssh_user = "my_user"       # 改为你的账号
ssh_passwd = "my_password" # 改为你的密码

try:
    os.remove("/tmp/control-channel")
except:
    pass

# 准备密码文件
file = open("./passwd","w")
passwd_content = f"#!/bin/sh\necho {ssh_passwd}"
file.write(passwd_content)
file.close()
os.chmod("./passwd", stat.S_IRWXU)

# 配置共享SSH套接字,后台运行
env = {'SSH_ASKPASS': "./passwd", 'DISPLAY':'', 'SSH_ASKPASS_REQUIRE':'force'}
args = ['ssh', '-f', '-o', 'LogLevel=ERROR', '-x', '-o', 'ConnectTimeout=30', '-o', 'ControlPersist=300', '-o', 'UserKnownHostsFile=/dev/null', '-o', 'StrictHostKeyChecking=no', '-o', 'ServerAliveInterval=15', '-MN', '-S', '/tmp/control-channel', '-p', '22', '-l', ssh_user, 'localhost']
process = subprocess.Popen(args, env=env,
        stdout=subprocess.PIPE,
#        stderr=subprocess.STDOUT,   # 取消注释启用stderr会导致subprocess挂起
        stdin=subprocess.DEVNULL,
        start_new_session=True)
sout, serr = process.communicate()
print(sout)
print(serr)

# 使用共享套接字
args2 = ['ssh', '-o', 'LogLevel=ERROR', '-o', 'ControlPath=/tmp/control-channel', '-p', '22', '-l', ssh_user, 'localhost', 'uname -a']
process2 = subprocess.Popen(args2,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        stdin=subprocess.DEVNULL)
content, _ = process2.communicate()
print(content)

正常执行结果

$ python3 test.py
b''
None
b'Linux shmachine 4.19.0-21-amd64 #1 SMP Debian 4.19.249-2 (2022-06-30) x86_64 GNU/Linux\n'

异常情况

取消注释第一个subprocess中的stderr=subprocess.STDOUT后,进程会挂起:

$ python3 test.py
^CTraceback (most recent call last):
  File "test.py", line 29, in <module>
    sout, serr = process.communicate()
  File "/usr/lib/python3.7/subprocess.py", line 926, in communicate
    stdout = self.stdout.read()
KeyboardInterrupt

环境信息

$ python3 --version
Python 3.7.3
$ ssh -V
OpenSSH_7.9p1 Debian-10+deb10u2, OpenSSL 1.1.1n  15 Mar 2022
$ cat /etc/issue
Debian GNU/Linux 10 \n \l

补充:将communicate()改为wait()可正常运行,但需明确communicate()失效的原因。


解答

问题根源在于ssh -f的行为与subprocess管道的交互逻辑冲突:

  1. ssh -f的工作机制:使用-f参数时,SSH会在认证完成后fork一个后台控制进程,原父进程随即退出。但当你开启stderr=subprocess.STDOUT时,SSH父进程在fork前可能会尝试向stderr写入数据,此时Python端的管道读取缓冲区未被及时消费,导致SSH父进程阻塞,进而触发整个流程挂起。

  2. communicate()的特性限制:communicate()会持续读取stdout和stderr的数据,直到收到EOF才会返回。而SSH fork出的后台子进程会继承管道文件描述符,且不会主动关闭这些管道,导致stdout/stderr永远无法触发EOF,communicate()会一直处于等待状态。

  3. wait()正常的原因:wait()仅等待SSH父进程退出,不会尝试读取管道数据。SSH父进程完成fork后会正常退出,因此wait()能顺利拿到退出状态,不会阻塞。

解决办法

若需捕获stderr同时避免挂起,可采用以下方案:

  • 移除-f参数:你的代码中已设置ControlPersist=300,该参数已能让控制进程在后台保持连接,无需-f进行fork操作。
  • 添加超时机制:使用process.communicate(timeout=5)强制终止等待,但需额外处理超时异常。

修改后的启动命令(移除-f):

args = ['ssh', '-o', 'LogLevel=ERROR', '-x', '-o', 'ConnectTimeout=30', '-o', 'ControlPersist=300', '-o', 'UserKnownHostsFile=/dev/null', '-o', 'StrictHostKeyChecking=no', '-o', 'ServerAliveInterval=15', '-MN', '-S', '/tmp/control-channel', '-p', '22', '-l', ssh_user, 'localhost']

此时即使开启stderr=subprocess.STDOUT,communicate()也能正常完成——SSH会直接启动控制进程,认证完成后进入后台等待,stdout和stderr会正常关闭,communicate()能收到EOF并返回。


内容的提问来源于stack exchange,提问作者atline

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 17:28:11