【发布时间】:2020-11-05 03:37:02
【问题描述】:
我正在尝试在 PyQt5 应用程序中异步执行 powershell 命令。 (不是PySide2)。为此,我创建了一个 subprocess 和 asyncio 来执行命令,并创建了 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