如何在 Windows 上崩溃后保持 subprocess.Popen 控制台打开?

How to keep subprocess.Popen console open after crashing on Windows?

我的目标是为每个 Popen 进程使用几个不同的参数调用主程序,每个进程都有自己的控制台 window。然而,一旦遇到崩溃,它就会关闭那个控制台,我真的很想让它保持打开状态。

import subprocess
from subprocess import CREATE_NEW_CONSOLE
import time

for i in range(1, 5):
    subprocess.Popen(["python", "main.py", str(i), str(i)], close_fds=False, creationflags=CREATE_NEW_CONSOLE)
    time.sleep(3)

是否可以使用 subprocess.Popen 创建一个新进程并保持控制台打开以便我读取错误? Link 到 subprocess docs.

控制台 window 由 conhost.exe (Win 7+) 的实例托管,当没有进程附加到它时退出。所以你只需要将第二个 python.exe 进程附加到每个控制台并让它等待。这是一个简单的演示脚本,它使等待进程等待主进程(即工作进程的父进程):

import sys

# If started with a pid, wait for the associated process to exit.
if len(sys.argv) > 1:
    import ctypes
    kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
    SYNCHRONIZE = 0x00100000
    pid = int(sys.argv[1])
    hproc = kernel32.OpenProcess(SYNCHRONIZE, 0, pid)
    if not hproc:
        sys.exit(ctypes.get_last_error())
    print('waiter: waiting')
    kernel32.WaitForSingleObject(hproc, -1)
    sys.exit(0)

import os
import subprocess
print('worker: start')
# The waiter waits on our parent (ppid).
p = subprocess.Popen([sys.executable, sys.argv[0], str(os.getppid())])
print('worker: exit')

例如:

>>> import sys, subprocess
>>> flags = subprocess.CREATE_NEW_CONSOLE
>>> p = subprocess.Popen([sys.executable, 'main.py'], creationflags=flags)
>>> p.wait()
0
>>> exit()

工作进程重新生成自身以等待其父进程。然后它以 return 代码 0 退出。控制台 window 在 exit() 后关闭。

您还可以从主进程生成服务员实例。在这种情况下,将工人的 PID 传递给服务员。然后服务员可以调用 FreeConsole and AttachConsole 将自己附加到工作人员的控制台。这样比较复杂,但是可以方便主进程终止waiter。

通过更改 stdout 和 stderr,您实际上可以获得所需的信息,例如使用 pdb (python's built in debugging library)。我喜欢这种方法,而不是保持 window 打开,因为它可以移植到无头环境。

根据 Popen docsstdin 设置为 subprocess.PIPE 并将 stderr 设置为 subprocess.STDOUT 如果您希望在同一缓冲区中捕获常规输出和错误输出.我个人更喜欢这个用于调试。否则将两者都设置为 PIPE.
不要忘记跟踪您的 Popen 对象,否则您无法检查它们。你的例子看起来像这样:

import subprocess
import time
import pdb

processes = []
for i in range(1, 5):
    process = subprocess.Popen(["python", "main.py", str(i), str(i)], 
                     close_fds=False, 
                     creationflags=subprocess.CREATE_NEW_CONSOLE,
                     stdout=subprocess.PIPE,
                     stderr=subprocess.STDOUT)
    processes.append(process)
    time.sleep(3)
pdb.set_trace()

请注意,创建的控制台不会显示输出,因为它已被重新路由。
现在,当您进入调试器时,进程应该是 subprocess.Popen 个对象的列表,例如(您的列表当然会更长):

(Pdb) processes
[<subprocess.Popen object at 0x000002D359BD6AF0>, <subprocess.Popen object at 0x000002D359BD6F10>]

轮询进程以找出哪些进程已退出:

(Pdb) processes[0].poll()
1
(Pdb) processes[1].poll()
(Pdb)

这个输出告诉我们列表中的第一个进程已经退出,第二个进程仍然是 运行。 如果你想要一个漂亮的打印输出捕获,请执行以下操作:

(Pdb) print(processes[0].stdout.read().decode())
Traceback (most recent call last):
  File "C:\Projects\dicom-nifti-message-service\tests\dicom_nifti_app.py", line 12, in <module>
    from conftest import set_progress
  File "C:\Projects\dicom-nifti-message-service\tests\conftest.py", line 46, in <module>
      from tests.dicom_nifti_app_config import config as config_
  ModuleNotFoundError: No module named 'tests.dicom_nifti_app_config'

这个命令看起来有点绕,但是 Popen.stdout 属性是一个缓冲区,你可以在它上面调用 read() 来得到它作为字节串。现在你调用 decode() 来得到一个字符串。使用 print() 在输出中看不到像 \n 这样的字符,但实际上将它们解释为换行符等

小提示:请注意,要在调试时访问 运行 进程的输出,您必须先将其杀死!按如下方式进行,或查看文档以了解其他方法。

(Pdb) processes[1].poll()
(Pdb) processes[1].kill()
(Pdb) processes[1].poll()
1
(Pdb)

如果您对实时调试不感兴趣,您可以:

  • 使用此方法将捕获的输出写入文件
  • 或者甚至更简单,从您在子进程运行中的程序
  • 登录到文件