通过 subprocess 将数据传递给 stdin 时,stdout 为空,但手动输入时包含预期输出?

`stdout` is empty when passing data to `stdin` via `subprocess`, but contains the expected output when input is entered manually?

所以我正在尝试从 Leela Chess Zero 引擎接收国际象棋移动数据。我已经完成了后端的所有 UI 和其他部分,这是我需要实现的最后一件事。不幸的是,我似乎高估了 Python 中子处理的简单程度...

为了解释更多的背景,我需要做的是:

  1. Call/run lc0.exe 在本地目录。我做得很好。
  2. 按顺序通过标准输入传递命令 position startpos e2e4 e7e5 e1e2go nodes 100quit。根据我可以通过 stderr 衡量的情况,这似乎也能正常工作。
  3. Receive/read 标准输出。这就是我卡住的地方。

以下是我迄今为止尝试过的方法:

>>> from subprocess import Popen, PIPE, STDOUT
>>> p = Popen(['lc0'], stdout=PIPE, stdin=PIPE, stderr=PIPE)
>>> stdout_data = p.communicate(input=b'position startpos e2e4 e7e5 e1e2\ngo nodes 100\nquit')[0]
>>> stdout_data
b''

我得到一个空字节串。然后我尝试了另一种方法作为测试:

>>> import subprocess
>>> subprocess.check_output(['lc0'], stderr=PIPE) #the following 3 lines is me typing into stdin
position startpos e2e4 e7e5 e1e2
go nodes 100
quit
b'info depth 1 seldepth 2 time 4081 nodes 4 score cp 12 nps 133 tbhits 0 pv e2e4 c7c5\r\ninfo depth 2 seldepth 3 time 4116 nodes 11 score cp 13 nps 166 tbhits 0 pv e2e4 c7c5 g1f3\r\ninfo depth 3 seldepth 4 time 4151 nodes 25 score cp 13 nps 247 tbhits 0 pv e2e4 c7c5 g1f3 e7e6\r\ninfo depth 3 seldepth 5 time 4218 nodes 68 score cp 13 nps 407 tbhits 0 pv e2e4 c7c5 b1c3 b8c6 g1e2\r\ninfo depth 4 seldepth 6 time 4312 nodes 134 score cp 13 nps 513 tbhits 0 pv e2e4 c7c5 b1c3 b8c6 g1f3 e7e5\r\nbestmove e2e4 ponder c7c5\r\n'

尤里卡!我从 stdout 收到了正确的输出。现在是时候以编程方式进行了:

>>> subprocess.check_output(['lc0'], stderr=PIPE, input=b'position startpos e2e4 e7e5 e1e2\ngo nodes 100\nquit')
b''

混蛋!这里发生了什么?我可以通过删除 stderr=PIPE 参数来确认所有命令显然确实是由引擎 运行 执行的,但是说完一切后,当我以编程方式传递命令时,stdout 为空。我也试过使用 subprocess.stdin.write() 得到相同的结果。

经过大量挖掘,我发现 pexpect 可能更适合这个用例。我安装了 pip install wexpect 瞧瞧:

>>> from wexpect import spawn
>>> child = spawn("lc0")

...

是的,它只是挂起。打破 ^C 给了我例外 pywintypes.error: (2, 'CreateFile', 'The system cannot find the file specified.'),所以我对使用 pexpect 而不是 subprocess 感到不太自信,因为我似乎更接近后者的解决方案。

无论如何,我确信我在某种程度上误用了 subprocess,但我究竟做错了什么?为什么我在通过 stdin 手动传递命令时正确接收 stdout,但在使用 input= 参数时却没有?

我们可以使用stdin.write() 一次一个地向引擎发送命令。还可以使用 moves 正确发送位置命令,如下所示: position startpos moves m1, m2 ...安全退出引擎并正确终止进程。

代码

from subprocess import Popen, PIPE, STDOUT, TimeoutExpired


def ecommand(p, comm):
    p.stdin.write(f'{comm}\n')


def analyze(efile):
    bestmove = '0000'

    p = Popen([efile], stdout=PIPE, stdin=PIPE, stderr=STDOUT, bufsize=0, text=True)  # stderr=STDOUT, also send stderr to stdout to see everything in stdout

    ecommand(p, 'position startpos moves e2e4 e7e5 e1e2')
    ecommand(p, 'go nodes 3000')

    for line in iter(p.stdout.readline, ''):  # read each line of engine output as replies from our command
        line = line.strip()
        print(line)

        if line.startswith('bestmove'):  # exit the loop when we get the engine bestmove
            bestmove = line.split()[1].strip()
            break

    ecommand(p, 'quit')  # properly quit the engine

    # Make sure process 'p' is terminated (if not terminated for some reason) as we already sent the quit command.
    try:
        p.communicate(timeout=5)
    except TimeoutExpired:  # If timeout has expired and process is still not terminated.
        p.kill()
        p.communicate()

    return bestmove


efile = 'E:\Chess_Engines\Lc0\lc0-v0.28.0-windows-cpu-openblas\lc0.exe'
bestmove = analyze(efile)
print(f'best move: {bestmove}')

输出

_
|   _ | |
|_ |_ |_| v0.28.0 built Aug 25 2021
Detected 4 core(s) and 8 thread(s) in 1 group(s).
...

info depth 1 seldepth 2 time 181 nodes 2 score cp 88 nps 142 tbhits 0 pv g8f6 d2d3
info depth 2 seldepth 3 time 200 nodes 3 score cp 89 nps 90 tbhits 0 pv g8f6 d2d3 d7d5
info depth 2 seldepth 4 time 236 nodes 5 score cp 93 nps 72 tbhits 0 pv g8f6 b1c3 b8c6 d2d3
info depth 3 seldepth 5 time 305 nodes 15 score cp 99 nps 107 tbhits 0 pv g8f6 d2d3 d7d5 b1c3 d5d4
info depth 3 seldepth 6 time 368 nodes 23 score cp 96 nps 114 tbhits 0 pv g8f6 d2d3 d7d5 b1c3 d5d4 c3b1
info depth 4 seldepth 7 time 466 nodes 34 score cp 105 nps 113 tbhits 0 pv g8f6 d2d3 d7d5 b1c3 d5d4 c3b1 b8c6
info depth 4 seldepth 8 time 561 nodes 55 score cp 106 nps 139 tbhits 0 pv g8f6 d2d3 d7d5 b1c3 d5d4 c3b1 b8c6
info depth 5 seldepth 8 time 871 nodes 105 score cp 109 nps 149 tbhits 0 pv g8f6 d2d3 d7d5 b1c3 b8c6 c1g5 c8e6 g1f3
info depth 5 seldepth 9 time 965 nodes 119 score cp 111 nps 149 tbhits 0 pv g8f6 d2d3 d7d5 b1c3 b8c6 c1g5 c8e6 g1f3
info depth 5 seldepth 10 time 1327 nodes 179 score cp 109 nps 154 tbhits 0 pv g8f6 d2d3 d7d5 b1c3 b8c6 c1g5 c8e6 g1f3 d5d4
info depth 5 seldepth 11 time 1444 nodes 206 score cp 110 nps 161 tbhits 0 pv g8f6 d2d3 d7d5 b1c3 b8c6 c1g5 c8e6 g1f3 d5d4 c3b1
info depth 5 seldepth 12 time 1621 nodes 240 score cp 110 nps 165 tbhits 0 pv g8f6 d2d3 d7d5 b1c3 b8c6 c1g5 c8e6 g1f3 d5d4 c3b1
info depth 6 seldepth 12 time 2126 nodes 331 score cp 112 nps 168 tbhits 0 pv g8f6 d2d3 d7d5 b1c3 b8c6 c1g5 c8e6 g1f3 d5d4 c3b1
info depth 6 seldepth 13 time 2582 nodes 430 score cp 111 nps 178 tbhits 0 pv g8f6 d2d3 d7d5 b1c3 b8c6 c1g5 c8e6 g1f3 d5d4 c3b1
info depth 6 seldepth 14 time 5050 nodes 1037 score cp 111 nps 212 tbhits 0 pv g8f6 d2d3 d7d5 b1c3 b8c6 c1g5 c8e6 e4d5 c6d4 e2d2 e6d5
info depth 7 seldepth 14 time 5569 nodes 1161 score cp 111 nps 214 tbhits 0 pv g8f6 d2d3 d7d5 b1c3 b8c6 c1g5 c8e6 e4d5 c6d4 e2d2 e6d5
info depth 7 seldepth 15 time 8385 nodes 1938 score cp 109 nps 235 tbhits 0 pv g8f6 d2d3 d7d5 b1c3 b8c6 c1g5 c6d4 e2e1 c7c6 g1f3 d8b6 a1b1 c8e6 f3e5
info depth 7 seldepth 15 time 8642 nodes 2007 score cp 109 nps 236 tbhits 0 pv g8f6 d2d3 d7d5 b1c3 b8c6 c1g5 c6d4 e2e1 c7c6 g1f3 d8b6 a1b1 c8e6 f3e5
bestmove g8f6 ponder d2d3
best move: g8f6