【问题标题】:PyQt core application doesn't return to caller on quitPyQt 核心应用程序在退出时不会返回给调用者
【发布时间】:2016-05-20 06:10:00
【问题描述】:

我有一个非 GUI PyQt 应用程序,我想在退出事件循环后执行一些清理任务并打印一条消息。

def main(name):
    signal( SIGTERM, onQuit )
    signal( SIGINT, onQuit )

    logging.basicConfig( level=LOG_LEVEL, format=LOG_FMT.format(name) )

    try:
        app = QtCore.QCoreApplication(sys.argv)
        QtDBus.QDBusConnection.sessionBus().registerService( "{}.{}.{}".format( DBUS_SERVICE,
                                                                                name,
                                                                                "CommandRelay" ) )
        relay = CommandRelay(name)
    except Exception:
        log.error( "Failed to create CommandRelay. process exiting." )
        raise
    try:
        app.exec_()
    except Exception:
        log.error( "Fatal exception occurred while running relay. Exiting." )
        raise
    finally:
        relay.destroy()
        log.info( "Exiting process now." )

此应用程序作为更大应用程序的子进程运行。当它收到SIGTERM 信号时,它会退出应用程序。

def onQuit( signum, stackframe ):
    """ Handle terminate signal """
    try:
        log.info( "Terminate signal received." )
        QtCore.QCoreApplication.quit()
    except Exception:
        log.exception( "Exception occured while terminating" )
        sys.exit(1)
    sys.exit(0)

根据QCoreApplication.quit() 的文档,程序应该在调用exec_() 之后将控制权返回给。但相反,它似乎立即退出。

平台是 Opensuse 13.2 x86_64。

【问题讨论】:

  • 说明您使用的平台可能会有所帮助。不同的平台可能有不同的行为。

标签: python pyqt pyqt4


【解决方案1】:

您的问题可能是 Python 在运行 Python 代码时处理信号 - 基本上类似于:

  • 是否出现信号?
    • 如果是,请调用信号处理程序
  • 执行下一次执行

但是,当您的应用程序在 Qt 主循环中时,它在 C++ 代码中,因此 Python 永远没有机会对信号做出反应。

我知道该问题的两种解决方案:

轮询信号

这只是使用QTimer 在几秒钟内执行一点 Python 代码:

timer = QTimer()
timer.timeout.connect(lambda: None)
timer.start(1000)

缺点是对信号做出反应最多需要 1 秒,而且你“毫无意义”地每秒运行一次代码 - 一直唤醒 CPU 可能是一个问题,例如功耗也是如此,但我没有任何证据支持这种说法,这更像是一种猜测。

但它是唯一适用于 Windows 的解决方案。

使用唤醒文件描述符轮询信号

这使用带有管道的QSocketNotifier 和signal.set_wakeup_fd 来监听信号。

结合方法

我在应用程序中处理信号的完整代码如下所示:

class SignalHandler(QObject):

    """Handler responsible for handling OS signals (SIGINT, SIGTERM, etc.).

    Attributes:
        _activated: Whether activate() was called.
        _notifier: A QSocketNotifier used for signals on Unix.
        _timer: A QTimer used to poll for signals on Windows.
        _orig_handlers: A {signal: handler} dict of original signal handlers.
        _orig_wakeup_fd: The original wakeup filedescriptor.
    """

    def __init__(self, *, app, quitter, parent=None):
        super().__init__(parent)
        self._notifier = None
        self._timer = usertypes.Timer(self, 'python_hacks')
        self._orig_handlers = {}
        self._activated = False
        self._orig_wakeup_fd = None

    def activate(self):
        """Set up signal handlers.

        On Windows this uses a QTimer to periodically hand control over to
        Python so it can handle signals.

        On Unix, it uses a QSocketNotifier with os.set_wakeup_fd to get
        notified.
        """
        self._orig_handlers[signal.SIGINT] = signal.signal(
            signal.SIGINT, self.interrupt)
        self._orig_handlers[signal.SIGTERM] = signal.signal(
            signal.SIGTERM, self.interrupt)

        if os.name == 'posix' and hasattr(signal, 'set_wakeup_fd'):
            # pylint: disable=import-error,no-member,useless-suppression
            import fcntl
            read_fd, write_fd = os.pipe()
            for fd in (read_fd, write_fd):
                flags = fcntl.fcntl(fd, fcntl.F_GETFL)
                fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
            self._notifier = QSocketNotifier(
                read_fd, QSocketNotifier.Read, self)
            self._notifier.activated.connect(self.handle_signal_wakeup)
            self._orig_wakeup_fd = signal.set_wakeup_fd(write_fd)
        else:
            self._timer.start(1000)
            self._timer.timeout.connect(lambda: None)
        self._activated = True

    def deactivate(self):
        """Deactivate all signal handlers."""
        if not self._activated:
            return
        if self._notifier is not None:
            self._notifier.setEnabled(False)
            rfd = self._notifier.socket()
            wfd = signal.set_wakeup_fd(self._orig_wakeup_fd)
            os.close(rfd)
            os.close(wfd)
        for sig, handler in self._orig_handlers.items():
            signal.signal(sig, handler)
        self._timer.stop()
        self._activated = False

    @pyqtSlot()
    def handle_signal_wakeup(self):
        """Handle a newly arrived signal.

        This gets called via self._notifier when there's a signal.

        Python will get control here, so the signal will get handled.
        """
        logging.debug("Handling signal wakeup!")
        self._notifier.setEnabled(False)
        read_fd = self._notifier.socket()
        try:
            os.read(read_fd, 1)
        except OSError:
            logging.exception("Failed to read wakeup fd.")
        self._notifier.setEnabled(True)

    def interrupt(self, signum, _frame):
        """Handler for signals to gracefully shutdown (SIGINT/SIGTERM)."""
        # [...]

【讨论】:

  • 对不起,我可能在这一点上不清楚。应用程序确实收到了 SIGTERM 信号并使用上述轮询方法处理它。处理程序调用 quit(),应用程序退出。但是,调用 exec_() 之后的代码永远不会执行。
  • 如果您之后删除sys.exit(0) 会发生什么?主循环可能不会立即退出(即当您调用退出时),但之后一段时间,当控制权回到 Qt 主循环时。
  • 这确实有效。使用 Tornado 事件循环,会处理 SystemExit 异常,以便在退出之前执行 finally 块。由于某种原因,它与 Qt 事件循环的工作方式不同。
  • 我想将您的答案标记为正确,但答案实际上在您的评论中。
猜你喜欢
  • 2014-06-04
  • 1970-01-01
  • 2010-11-27
  • 1970-01-01
  • 1970-01-01
  • 2019-09-18
  • 1970-01-01
  • 2017-04-14
  • 1970-01-01
相关资源
最近更新 更多