【问题标题】:python subprocess waiting for grandchild on Windows with stdout setpython子进程在设置了标准输出的Windows上等待孙子
【发布时间】:2019-08-05 05:13:55
【问题描述】:

我有一个脚本,它是自动化测试套件的一部分。它在 Windows 上运行非常缓慢,但在 Linux 上却没有,我已经找到了原因。我们正在测试的进程('frank')创建了一个子进程(所以是一个孙子进程)。在该孙子进程也结束之前,python 代码不会返回(在 Windows 上 - 在 Linux 上不这样做)。如果没有父进程,孙进程将在 5 秒后杀死自己(它会挂起以防另一个进程与它交谈)

我发现如果我不捕获标准输出,我可以阻止通信功能以这种方式挂起。但我需要标准输出。我在某处读到通信功能正在等待所有管道关闭。我知道孙子的标准输出句柄是重复的,但我无法更改我正在测试的代码。

我一直在寻找解决方案。我尝试了一些创建标志(仍在代码中)但没有帮助。

这是缩减测试-

import os
import sys
import threading
import subprocess


def read_from_pipe(process):
    last_stdout = process.communicate()[0]
    print (last_stdout)

CREATE_NEW_PROCESS_GROUP = 0x00000200
DETACHED_PROCESS = 0x00000008

# start process
command = 'frank my arguments'

cwd = "C:\\dev\\ui_test\\frank_test\\workspace\\report183"

p = subprocess.Popen(command,
                     stdout=subprocess.PIPE,
                     cwd=cwd)

# run thread to read from output
t = threading.Thread(target=read_from_pipe, args=[p])
t.start()
t.join(30)
print('finished')

有什么想法吗?

谢谢。

彼得。

【问题讨论】:

  • 不要打扰communicate,因为您只使用stdout。等待子进程退出,然后在句柄上调用PeekNamedPipe(来自msvcrt.get_osfhandle(p.stdout.fileno()))以获取可供读取的字节数。尝试读取比可用内容更多的内容会阻塞,因为它是一个同步模式文件,并且管道有另一个潜在的写入者,即孙进程。
  • 如果您需要获取的不仅仅是一个充满数据的管道(默认为 4 KiB),那么请使用一个循环来轮询 PeekNamedPipe 并在子进程运行时读取可用数据。
  • @eryksun 我还有点儿戏。如果我等待进程以“p.wait()”结束,我会遇到与通信相同的问题。它一直等到孙子完成。
  • 谢谢 - 我想我把你的“等待”这个词的字面意思是 .wait 调用!我现在查看了民意调查并通过一个小循环进行了尝试。这如我所愿——我的意思是,poll() 告诉我这个过程已经结束,即使孙子还活着。我会看看你的其余建议。我一直希望有一个简单的旗帜或其他东西! :-)
  • 我使用的是 python 3.x。我不确定究竟是哪个子版本,因为这个测试系统通常在一些我必须检查的构建机器上运行。

标签: python windows subprocess pipe stdout


【解决方案1】:

经过@eryksun 的提示和大量的谷歌搜索,我有很多相当复杂的代码!有一次,我考虑过作弊并做 os.system 并重定向到一个临时文件,但后来我意识到我们的测试代码允许命令超时。如果子进程没有死亡,os.system 将永远阻塞。

import os
import sys
import threading
import subprocess
import time
if os.name == 'nt':
    import msvcrt
    import ctypes

# See https://stackoverflow.com/questions/55160319/python-subprocess-waiting-for-grandchild-on-windows-with-stdout-set for details on Windows code
# Based on https://github.com/it2school/Projects/blob/master/2017/Python/party4kids-2/CarGame/src/pygame/tests/test_utils/async_sub.py

from ctypes.wintypes import DWORD
if sys.version_info >= (3,):
    null_byte = '\x00'.encode('ascii')
else:
    null_byte = '\x00'

def ReadFile(handle, desired_bytes, ol = None):
    c_read = DWORD()
    buffer = ctypes.create_string_buffer(desired_bytes+1)
    success = ctypes.windll.kernel32.ReadFile(handle, buffer, desired_bytes, ctypes.byref(c_read), ol)
    buffer[c_read.value] = null_byte
    return ctypes.windll.kernel32.GetLastError(), buffer.value


def PeekNamedPipe(handle):
    c_avail = DWORD()
    c_message = DWORD()
    success = ctypes.windll.kernel32.PeekNamedPipe(handle, None, 0, None, ctypes.byref(c_avail), ctypes.byref(c_message))
    return "", c_avail.value, c_message.value


def read_available(handle):
    buffer, bytesToRead, result = PeekNamedPipe(handle)
    if bytesToRead:
        hr, data = ReadFile(handle, bytesToRead, None)
        return data
    return b''

def read_from_pipe(process):
    if os.name == 'posix':
        last_stdout = process.communicate()[0]
    else:
        handle = msvcrt.get_osfhandle(process.stdout.fileno())
        last_stdout = b''
        while process.poll() is None:
            last_stdout += read_available(handle)
            time.sleep(0.1)
        last_stdout += read_available(handle)
    print (last_stdout)

# start process
command = 'frank my arguments'

cwd = "C:\\dev\\ui_test\\frank_test\\workspace\\report183"

p = subprocess.Popen(command,
                     stdout=subprocess.PIPE,
                     cwd=cwd)

# run thread to read from output
t = threading.Thread(target=read_from_pipe, args=[p])
t.start()
t.join(30)
print('finished')

【讨论】:

  • read_available 应该能够使用process.stdout 文件,如果您将Popen bufsize 设置为0 或通过stdout.raw.read 进行所有读取。
  • 对于PeekNamedPipe,使用kernel32 = ctypes.WinDLL('kernel32', use_last_error=True) 而不是ctypes.windll.kernel32。如果调用失败,则通过ctypes.get_last_error() 获取错误,这是由use_last_error=True 启用的更可靠的值。通过raise ctypes.WinError(ctypes.get_last_error()) 引发异常。匿名管道不会有任何未读消息字节;将其传递为None。您只需要可用字节数。通过将函数命名为 get_available_bytes 来表明这一点。
  • 你不需要线程,因为它只是标准输出。如果我们必须在同步模式下从 stdout 和 stderr 读取并且不能调用 PeekNamedPipe 来读取可用的内容,则 Windows 中需要线程以避免死锁。
猜你喜欢
  • 2014-04-01
  • 1970-01-01
  • 1970-01-01
  • 2011-03-08
  • 1970-01-01
  • 1970-01-01
  • 2011-08-29
  • 2021-05-28
相关资源
最近更新 更多