Use QSocketNotifier for userscripts.

This commit is contained in:
Florian Bruhin 2014-12-29 22:58:00 +01:00
parent 4471f81c11
commit ecc7f09f86

View File

@ -22,67 +22,44 @@
import os import os
import os.path import os.path
import tempfile import tempfile
import select
from PyQt5.QtCore import (pyqtSignal, QObject, QThread, QStandardPaths, from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QStandardPaths,
QProcessEnvironment, QProcess, QUrl) QSocketNotifier, QProcessEnvironment, QProcess, QUrl)
from qutebrowser.utils import message, log, objreg, standarddir from qutebrowser.utils import message, log, objreg, standarddir
from qutebrowser.commands import runners, cmdexc from qutebrowser.commands import runners, cmdexc
class _BlockingFIFOReader(QObject): class _QtFIFOReader(QObject):
"""A worker which reads commands from a FIFO endlessly. """A FIFO reader based on a QSocketNotifier."""
This is intended to be run in a separate QThread. It reads from the given
FIFO even across EOF so an userscript can write to it multiple times.
It uses select() so it can timeout once per second, checking if termination
was requested.
Attributes:
_filepath: The filename of the FIFO to read.
fifo: The file object which is being read.
Signals:
got_line: Emitted when a new line arrived.
finished: Emitted when the read loop realized it should terminate and
is about to do so.
"""
got_line = pyqtSignal(str) got_line = pyqtSignal(str)
finished = pyqtSignal()
def __init__(self, filepath, parent=None): def __init__(self, filepath, parent=None):
super().__init__(parent) super().__init__(parent)
self._filepath = filepath self._filepath = filepath
self.fifo = None # We open as R/W so we never get EOF and have to reopen the pipe.
# See http://www.outflux.net/blog/archives/2008/03/09/using-select-on-a-fifo/
# We also use os.open and os.fdopen rather than built-in open so we
# can add O_NONBLOCK.
fd = os.open(filepath, os.O_RDWR |
os.O_NONBLOCK) # pylint: disable=no-member
self.fifo = os.fdopen(fd, 'r')
self._notifier = QSocketNotifier(fd, QSocketNotifier.Read, self)
self._notifier.activated.connect(self.read_line)
def read(self): @pyqtSlot()
"""Blocking read loop which emits got_line when a new line arrived.""" def read_line(self):
try: """(Try to) read a line from the fifo."""
# We open as R/W so we never get EOF and have to reopen the pipe. self._notifier.setEnabled(False)
# See http://www.outflux.net/blog/archives/2008/03/09/using-select-on-a-fifo/ for line in self.fifo:
# We also use os.open and os.fdopen rather than built-in open so we self.got_line.emit(line.rstrip('\r\n'))
# can add O_NONBLOCK. self._notifier.setEnabled(True)
fd = os.open(self._filepath, os.O_RDWR |
os.O_NONBLOCK) # pylint: disable=no-member def cleanup(self):
self.fifo = os.fdopen(fd, 'r') """Clean up so the fifo can be closed."""
except OSError: self._notifier.setEnabled(False)
log.procs.exception("Failed to read FIFO")
self.finished.emit()
return
while True:
log.procs.debug("thread loop")
ready_r, _ready_w, _ready_e = select.select([self.fifo], [], [], 1)
if ready_r:
log.procs.debug("reading data")
for line in self.fifo:
self.got_line.emit(line.rstrip())
if QThread.currentThread().isInterruptionRequested():
self.finished.emit()
return
class _BaseUserscriptRunner(QObject): class _BaseUserscriptRunner(QObject):
@ -184,20 +161,17 @@ class _BaseUserscriptRunner(QObject):
class _POSIXUserscriptRunner(_BaseUserscriptRunner): class _POSIXUserscriptRunner(_BaseUserscriptRunner):
"""Userscript runner to be used on POSIX. Uses _BlockingFIFOReader. """Userscript runner to be used on POSIX. Uses _QtFIFOReader.
The OS must have support for named pipes and select(). Commands are Commands are executed immediately when they arrive in the FIFO.
executed immediately when they arrive in the FIFO.
Attributes: Attributes:
_reader: The _BlockingFIFOReader instance. _reader: The _QtFIFOReader instance.
_thread: The QThread where reader runs.
""" """
def __init__(self, win_id, parent=None): def __init__(self, win_id, parent=None):
super().__init__(win_id, parent) super().__init__(win_id, parent)
self._reader = None self._reader = None
self._thread = None
def run(self, cmd, *args, env=None): def run(self, cmd, *args, env=None):
rundir = standarddir.get(QStandardPaths.RuntimeLocation) rundir = standarddir.get(QStandardPaths.RuntimeLocation)
@ -205,7 +179,7 @@ class _POSIXUserscriptRunner(_BaseUserscriptRunner):
# tempfile.mktemp is deprecated and discouraged, but we use it here # tempfile.mktemp is deprecated and discouraged, but we use it here
# to create a FIFO since the only other alternative would be to # to create a FIFO since the only other alternative would be to
# create a directory and place the FIFO there, which sucks. Since # create a directory and place the FIFO there, which sucks. Since
# os.kfifo will raise an exception anyways when the path doesn't # os.mkfifo will raise an exception anyways when the path doesn't
# exist, it shouldn't be a big issue. # exist, it shouldn't be a big issue.
self._filepath = tempfile.mktemp(prefix='userscript-', dir=rundir) self._filepath = tempfile.mktemp(prefix='userscript-', dir=rundir)
os.mkfifo(self._filepath) # pylint: disable=no-member os.mkfifo(self._filepath) # pylint: disable=no-member
@ -214,41 +188,31 @@ class _POSIXUserscriptRunner(_BaseUserscriptRunner):
e)) e))
return return
self._reader = _BlockingFIFOReader(self._filepath) self._reader = _QtFIFOReader(self._filepath)
self._thread = QThread(self)
self._reader.moveToThread(self._thread)
self._reader.got_line.connect(self.got_cmd) self._reader.got_line.connect(self.got_cmd)
self._thread.started.connect(self._reader.read)
self._reader.finished.connect(self.on_reader_finished)
self._thread.finished.connect(self.on_thread_finished)
self._run_process(cmd, *args, env=env) self._run_process(cmd, *args, env=env)
self._thread.start()
def on_proc_finished(self): def on_proc_finished(self):
"""Interrupt the reader when the process finished.""" """Interrupt the reader when the process finished."""
log.procs.debug("proc finished") log.procs.debug("proc finished")
self._thread.requestInterruption() self.finish()
def on_proc_error(self, error): def on_proc_error(self, error):
"""Interrupt the reader when the process had an error.""" """Interrupt the reader when the process had an error."""
super().on_proc_error(error) super().on_proc_error(error)
self._thread.requestInterruption() self.finish()
def on_reader_finished(self): def finish(self):
"""Quit the thread and clean up when the reader finished.""" """Quit the thread and clean up when the reader finished."""
log.procs.debug("reader finished") log.procs.debug("Cleaning up")
self._thread.quit() self._reader.cleanup()
self._reader.fifo.close() self._reader.fifo.close()
self._reader.deleteLater() self._reader.deleteLater()
self._reader = None
super()._cleanup() super()._cleanup()
self.finished.emit() self.finished.emit()
def on_thread_finished(self):
"""Clean up the QThread object when the thread finished."""
log.procs.debug("thread finished")
self._thread.deleteLater()
class _WindowsUserscriptRunner(_BaseUserscriptRunner): class _WindowsUserscriptRunner(_BaseUserscriptRunner):