【问题标题】:Executing a subprocess in PyQt5 application never returns在 PyQt5 应用程序中执行子进程永远不会返回
【发布时间】:2020-11-05 03:37:02
【问题描述】:

我正在尝试在 PyQt5 应用程序中异步执行 powershell 命令。 (不是PySide2)。为此,我创建了一个 subprocessasyncio 来执行命令,并创建了 communicate() 函数来获取命令的输出。

process = await asyncio.create_subprocess_exec(args)
stdout, stderr = await process.communicate()

但是,我的应用程序永远不会超过 communicate() 函数。即使我加入了Timer 类来取消该过程。

首先我在没有PyQt5 的情况下制作了以下temp.py 脚本,看看它是否有效。

import asyncio
import logging
import subprocess
from functools import partial
from tempfile import NamedTemporaryFile

logging.basicConfig(
    format="%(asctime)s - %(name)s - %(levelname)s: %(message)s",
    datefmt="%H:%M:%S",
    level=logging.INFO,
)


class Timer:
    def __init__(self, timeout, callback):
        self.logger = logging.getLogger("TIMER")
        self._timeout = timeout
        self._callback = callback
        self._task = asyncio.create_task(self._job())
        self.logger.info("Timer started")

    async def _job(self) -> None:
        try:
            await asyncio.sleep(self._timeout)
            self.logger.info("Timer expired")
            self._callback()
        except asyncio.CancelledError:
            self.logger.info("Timer cancelled")
            pass

    def cancel(self) -> None:
        self.logger.info("Cancelling timer")
        self._task.cancel()


async def do_your_thing():

    logging.info("start")

    cmd = "Start-Sleep -s 5; echo 'sleeping done'"

    stdout_file = NamedTemporaryFile(mode="r+", delete=False)
    stderr_file = NamedTemporaryFile(mode="r+", delete=False)

    startupinfo = subprocess.STARTUPINFO()
    startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
    startupinfo.wShowWindow = subprocess.SW_HIDE

    process = await asyncio.create_subprocess_exec(
        "powershell.exe",
        cmd,
        stdout=stdout_file,
        stderr=stderr_file,
        shell=False,
        startupinfo=startupinfo,
    )

    def _cancel_process(process):
        logging.info("terminating process")
        process.terminate()

    # timer = Timer(1, partial(_cancel_process, process))
    stdout, stderr = await process.communicate()
    logging.info(f"after await process communicate: {stdout}\t{stderr}")

    stdout_file.seek(0)
    stdout_str = str(stdout_file.read())
    stdout_file.close()
    logging.info(f"stdout:\n{stdout_str}")
    stderr_file.seek(0)
    stderr_str = str(stderr_file.read())
    stderr_file.close()
    logging.info(f"stderr:\n{stderr_str}")


if __name__ == "__main__":

    asyncio.run(do_your_thing())

这行得通。

没有Timer:

python temp.py 
17:09:14 - root - INFO: start
17:09:19 - root - INFO: after await process communicate: None   None
17:09:19 - root - INFO: stdout:
sleeping done

17:09:19 - root - INFO: stderr:

17:09:19 - root - INFO: done

Timer

python temp.py 
17:10:22 - root - INFO: start
17:10:22 - TIMER - INFO: Timer started
17:10:23 - TIMER - INFO: Timer expired
17:10:23 - root - INFO: terminating process
17:10:23 - root - INFO: after await process communicate: None   None
17:10:23 - root - INFO: stdout:

17:10:23 - root - INFO: stderr:

17:10:23 - root - INFO: done

所以,我认为我的问题必须是PyQt5 如何处理进程。我做了这个小的temp2.pyPyQt5 应用程序来测试它。

import asyncio
import logging
import subprocess
from functools import partial
from tempfile import NamedTemporaryFile

import qasync
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton

logging.basicConfig(
    format="%(asctime)s - %(name)s - %(levelname)s: %(message)s",
    datefmt="%H:%M:%S",
    level=logging.INFO,
)


class Timer:
    def __init__(self, timeout, callback):
        self.logger = logging.getLogger("TIMER")
        self._timeout = timeout
        self._callback = callback
        self._task = asyncio.create_task(self._job())
        self.logger.info("Timer started")

    async def _job(self) -> None:
        try:
            await asyncio.sleep(self._timeout)
            self.logger.info("Timer expired")
            self._callback()
        except asyncio.CancelledError:
            self.logger.info("Timer cancelled")
            pass

    def cancel(self) -> None:
        self.logger.info("Cancelling timer")
        self._task.cancel()


class TestWindow(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.btn = QPushButton("Start")
        self.btn.clicked.connect(self.btn_clicked)

        self.setCentralWidget(self.btn)

    @qasync.asyncSlot()
    async def btn_clicked(self):

        await self.do_your_thing()

    @qasync.asyncSlot()
    async def do_your_thing(self):

        logging.info("start")

        cmd = "Start-Sleep -s 5; echo 'sleeping done'"

        stdout_file = NamedTemporaryFile(mode="r+", delete=False)
        stderr_file = NamedTemporaryFile(mode="r+", delete=False)

        startupinfo = subprocess.STARTUPINFO()
        startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
        startupinfo.wShowWindow = subprocess.SW_HIDE

        process = await asyncio.create_subprocess_exec(
            "powershell.exe",
            cmd,
            stdout=stdout_file,
            stderr=stderr_file,
            shell=False,
            startupinfo=startupinfo,
        )

        def _cancel_process(process):
            logging.info("terminating process")
            process.terminate()
            logging.info("terminated process")

        #timer = Timer(1, partial(_cancel_process, process))
        stdout, stderr = await process.communicate()
        logging.info(f"after await process communicate: {stdout}\t{stderr}")

        stdout_file.seek(0)
        stdout_str = str(stdout_file.read())
        stdout_file.close()
        logging.info(f"stdout:\n{stdout_str}")
        stderr_file.seek(0)
        stderr_str = str(stderr_file.read())
        stderr_file.close()
        logging.info(f"stderr:\n{stderr_str}")


if __name__ == "__main__":

    import sys

    app = QApplication(sys.argv)
    loop = qasync.QEventLoop(app)
    asyncio.set_event_loop(loop)
    main_window = TestWindow()
    main_window.show()
    sys.exit(app.exec_())

它不起作用。没有Timer,我得到以下输出:

python temp2.py 
17:13:02 - root - INFO: start

# I closed the GUI
C finished at Wed Jul 15 17:13:23

还有Timer

17:14:03 - root - INFO: start
17:14:03 - TIMER - INFO: Timer started
17:14:04 - TIMER - INFO: Timer expired
17:14:04 - root - INFO: terminating process
17:14:04 - root - INFO: terminated process

# I closed the GUI
C finished at Wed Jul 15 17:14:17

不知何故 process.communicate() 阻塞(但是我可以移动 GUI,所以不会真正阻塞)并且永远不会返回。我不知道如何解决这个问题。我究竟做错了什么?我应该以其他方式实现吗?

【问题讨论】:

  • 尝试不使用使用asyncio运行程序(只需使用proc = subprocess.Popen(...)启动命令并使用proc.communicate()调用是否会返回?请记住@ 987654348@等待子进程结束。
  • @Bakuriu 明天试试,谢谢。 proc.communicate() 等待没问题,因为我需要命令中的标准输出才能走得更远。它只是不能阻止其余部分,因此 GUI 不会进入not responding。 || does the call ever return? 是什么意思。不使用PyQt5 时返回,使用PyQt5 时不返回。您可以通过日志输出看到这一点。
  • @Bakuriu 使用 subprocess.Popen() proc.communicate() 返回,但它会阻止主 GUI 并且计时器不再工作。定时器在proc.communicate() 返回后开始休眠。不是我要找的。​​span>

标签: python python-3.x pyqt5 subprocess python-asyncio


【解决方案1】:

我最终完全放弃了子流程并开始尝试使用 QProcess。

import asyncio
import logging
import subprocess
from functools import partial
from tempfile import NamedTemporaryFile

import qasync
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton
from PyQt5.QtCore import QProcess

logging.basicConfig(
    format="%(asctime)s - %(name)s - %(levelname)s: %(message)s",
    datefmt="%H:%M:%S",
    level=logging.INFO,
)


class Timer:
    def __init__(self, timeout, callback):
        self.logger = logging.getLogger("TIMER")
        self._timeout = timeout
        self._callback = callback
        self._task = asyncio.create_task(self._job())
        self.logger.info("Timer started")

    async def _job(self) -> None:
        try:
            await asyncio.sleep(self._timeout)
            self.logger.info("Timer expired")
            self._callback()
        except asyncio.CancelledError:
            self.logger.info("Timer cancelled")
            pass

    def cancel(self) -> None:
        self.logger.info("Cancelling timer")
        self._task.cancel()


class TestWindow(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.btn = QPushButton("Start")
        self.btn.clicked.connect(self.btn_clicked)

        self.setCentralWidget(self.btn)

    def btn_clicked(self):

        self.do_your_thing()

    def proc_finished(
        self, process: QProcess, exit_code: int, exit_status: QProcess.ExitStatus
    ):

        logging.info(f"Proces finished: {exit_code}\t{exit_status}")
        logging.info(f"{process.readAllStandardOutput()}")
        logging.info(f"{process.readAllStandardError()}")

    def proc_error(self, error: QProcess.ProcessError):

        logging.info(f"Proces error: {error}")

    def do_your_thing(self):
        def _cancel_process(process):
            logging.info("terminating process")
            process.kill()
            logging.info("terminated process")

        logging.info("start")

        cmd = ["Start-Sleep -s 1; echo 'sleeping done'"]
        shell = "powershell.exe"

        process = QProcess(self)
        process.finished.connect(partial(self.proc_finished, process))
        process.errorOccurred.connect(self.proc_error)

        process.start(shell, cmd)
        Timer(2, partial(_cancel_process, process))


if __name__ == "__main__":

    import sys

    app = QApplication(sys.argv)
    loop = qasync.QEventLoop(app)
    asyncio.set_event_loop(loop)
    main_window = TestWindow()
    main_window.show()
    sys.exit(app.exec_())

或者如果你想在同一个函数中等待直到进程完成

    async def do_your_thing(self):
        def _cancel_process(process):
            logging.info("terminating process")
            process.kill()
            logging.info("terminated process")

        logging.info("start")

        cmd = ["Start-Sleep -s 1; echo 'sleeping done'"]
        shell = "powershell.exe"

        process = QProcess(self)
        process.finished.connect(partial(self.proc_finished, process))
        process.errorOccurred.connect(self.proc_error)

        process.start(shell, cmd)
        Timer(2, partial(_cancel_process, process))

        while process.state() != QProcess.NotRunning:
            await asyncio.sleep(0)

        timer.cancel()
        if process.exitCode() == 0:
            stdout = process.readAllStandardOutput()
            logging.info(stdout)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-10-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多