Windows 上的 Python 子进程:启动子进程“cmd.exe”并为其提供 bat 文件,停止主进程执行

Posted

技术标签:

【中文标题】Windows 上的 Python 子进程:启动子进程“cmd.exe”并为其提供 bat 文件,停止主进程执行【英文标题】:Python subprocess on Windows: Launching subprocess "cmd.exe" and giving it a bat-file, stops the main process execution 【发布时间】:2020-01-01 21:31:18 【问题描述】:

我打算使用 Python 中的 subprocess 模块运行带有 cmd.exe 的 Windows .bat。为简单起见,我创建了一个可重现的最小示例,您可以在自己的计算机上运行该示例。 首先,我将展示它的设置。然后你会得到代码,然后我解释出了什么问题。

1。设置

我有一台安装了Python 3.7PyQt5 的Windows 10 计算机。对于可重现的示例,我创建了一个包含三个文件的文件夹:

文件 my_subprocess.py 是一个 python 脚本,它生成一个带有显示计数器标签的窗口。 my_subprocess.bat 文件是一个非常简单的 bat 文件,它执行这个 python 文件。

 my_mainprocess.py 文件使用 Python subprocess 模块来运行 cmd.exe 中的 bat 文件。子进程启动后,一直监听子进程的输出通道,并在窗口中显示输出:

2。代码

my_subprocess.py:

from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import sys

class SubprocessWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setGeometry(100, 100, 600, 300)
        self.setWindowTitle("MY SUBPROCESS WINDOW")
        # 1. OUTER FRAME
        self.__frm = QFrame()
        self.__frm.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self.__frm.setStyleSheet("QFrame  background-color:#eeeeec; border-color: #2e3436; ")
        self.__lyt = QVBoxLayout()
        self.__frm.setLayout(self.__lyt)
        self.setCentralWidget(self.__frm)
        self.show()
        # 2. WIDGETS
        self.__cntr = 0
        self.__myLabel = QLabel(f"cntr: self.__cntr")
        self.__myLabel.setFixedHeight(40)
        self.__myLabel.setFixedWidth(200)
        self.__myLabel.setStyleSheet("QLabel  background-color:#ffffff; border-color: #2e3436; border-width: 1px; border-style: solid; font: 16pt; ")
        self.__lyt.addWidget(self.__myLabel)
        self.count()
        return

    def count(self):
        self.__cntr += 1
        self.__myLabel.setText(f"cntr: self.__cntr")
        print(f"cntr: self.__cntr")
        QTimer.singleShot(500, self.count)
        return

if __name__== '__main__':
    app = QApplication(sys.argv)
    QApplication.setStyle(QStyleFactory.create('Plastique'))
    myGUI = SubprocessWindow()
    sys.exit(app.exec_())

my_subprocess.bat:

@echo off
ECHO Start Subprocess
SETLOCAL
python my_subprocess.py
ECHO Finish Subprocess

 my_mainprocess.py:

from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import sys, os, subprocess

class MainprocessWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setGeometry(100, 500, 600, 300)
        self.setWindowTitle("MY MAIN PROCESS WINDOW")
        # 1. OUTER FRAME
        self.__frm = QFrame()
        self.__frm.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self.__frm.setStyleSheet("QFrame  background-color:#fce94f; border-color: #2e3436; ")
        self.__lyt = QVBoxLayout()
        self.__frm.setLayout(self.__lyt)
        self.setCentralWidget(self.__frm)
        self.show()
        # 2. WIDGETS
        self.__cntr = 0
        self.__myTextEdit = QPlainTextEdit()
        self.__myTextEdit.setStyleSheet("""
                QPlainTextEdit 
                    color: #ffffffff;
                    font: 12pt;
                    background: #ff000000;
                    border-width: 1px;
                    border-color: #ff888a85;
                    border-style: solid;
                    border-radius: 2px;
                    padding: 1px;
                    margin: 5px 5px 5px 5px;
                
                """)
        self.__myTextEdit.setFixedHeight(290)
        self.__myTextEdit.setFixedWidth(590)
        self.__lyt.addWidget(self.__myTextEdit)
        self.__proc = None
        QTimer.singleShot(1000, self.start_subprocess)
        return

    def start_subprocess(self):
        self.activateWindow()
        self.__proc = subprocess.Popen("cmd.exe", shell=True, universal_newlines=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        command = "my_subprocess.bat\n"
        self.__proc.stdin.write(command)
        self.__proc.stdin.flush()
        QTimer.singleShot(100, self.catch_output)
        return

    def catch_output(self):
        if self.__proc.poll() is None:
            self.__proc.stdout.flush()
            output_msg = self.__proc.stdout.readline()
            self.__myTextEdit.appendPlainText(output_msg)
        else:
            self.__myTextEdit.appendPlainText("Subprocess dead")
        QTimer.singleShot(300, self.catch_output)
        return

if __name__== '__main__':
    app = QApplication(sys.argv)
    QApplication.setStyle(QStyleFactory.create('Plastique'))
    myGUI = MainprocessWindow()
    sys.exit(app.exec_())

3。运行代码

请将代码复制粘贴到文件 my_subprocess.pymy_subprocess.batmy_mainprocess.py 中。打开控制台,然后导航到包含这些文件的文件夹。运行python my_mainprocess.py 启动主进程,然后启动子进程。您应该会看到顶部显示的两个窗口:第一个属于子进程,第二个属于主进程。

 

4。问题

4.1 主进程挂起 我相信主进程和子进程都应该同时运行。看主进程的代码。任何地方都没有阻塞呼叫。它只是捕获stdout 通道上的输出并将其打印在窗口上:

def catch_output(self):
    if self.__proc.poll() is None:
        self.__proc.stdout.flush()
        output_msg = self.__proc.stdout.readline()
        self.__myTextEdit.appendPlainText(output_msg)
    else:
        self.__myTextEdit.appendPlainText("Subprocess dead")
    QTimer.singleShot(300, self.catch_output)
    return

但是,主进程会挂起,直到您停止子进程(通过关闭子进程窗口)。然后,主进程继续并打印所有输出。为什么?

4.2 主进程崩溃 事情变得更糟了。主进程完成标准输出通道的输出后,就崩溃了。 我相信我知道为什么 - 但我没有解决方案。如果子进程处于活动状态,则调用 self.__proc.poll() 应该返回 None,否则返回一个数字。相反,我注意到它只是不断返回None,即使子进程已经死了并且很久以前就被埋没了。 如何规避这个问题?


 更新: 在每个打印语句之后的my_subprocess.py 代码中添加sys.stdout.flush() 肯定会有所帮助。但这不是一个好的解决方案,因为my_subprocess.py 只是任何可以作为子进程运行的潜在程序的代表。对于大多数此类程序,我们无权访问源代码。 换句话说,解决方案应该只关注my_mainprocess.py可以做的事情。

【问题讨论】:

【参考方案1】:

MainprocessWindow.start_subprocess 更改为:

    def start_subprocess(self):
        self.activateWindow()
        command = "cmd.exe /c my_subprocess.bat\n"
        self.__proc = subprocess.Popen(command, shell=True, universal_newlines=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        QTimer.singleShot(100, self.catch_output)
        return

并将行 sys.stdout.fulsh() 添加到 SubprocessWindow.count,如下所示:

    def count(self):
        self.__cntr += 1
        self.__myLabel.setText(f"cntr: self.__cntr")
        print(f"cntr: self.__cntr")
        sys.stdout.flush()
        QTimer.singleShot(500, self.count)
        return

【讨论】:

嗨@Matic,您关于以不同方式启动子流程的建议肯定很有帮助。但是,我不能将sys.stdout.flush() 添加到子流程代码本身。这是因为这个子进程代表了任何可以作为子进程运行的潜在程序。我忘了在我的问题中提到这一点。

以上是关于Windows 上的 Python 子进程:启动子进程“cmd.exe”并为其提供 bat 文件,停止主进程执行的主要内容,如果未能解决你的问题,请参考以下文章

在 Windows 上的 Python Popen 子进程中暂停 FFmpeg 编码

如何通过 python 子进程与 mac 上的应用程序交互?

Python:当父进程死亡时如何杀死子进程?

无法在 Windows 上使用 Python 终止正在运行的子进程

windows下启动的进程都是explorer的子进程吗

如何在 Windows 上通过 QProcess 启动提升的子进程?