Merge branch 'userscript-runner'

This commit is contained in:
Florian Bruhin 2015-01-04 15:21:31 +01:00
commit 04c8a17b2e
2 changed files with 46 additions and 74 deletions

View File

@ -822,6 +822,7 @@ class CommandDispatcher:
cmd: The userscript to run. cmd: The userscript to run.
args: Arguments to pass to the userscript. args: Arguments to pass to the userscript.
""" """
cmd = os.path.expanduser(cmd)
userscripts.run(cmd, *args, url=self._current_url(), userscripts.run(cmd, *args, url=self._current_url(),
win_id=self._win_id) win_id=self._win_id)

View File

@ -22,67 +22,45 @@
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. log.procs.debug("QSocketNotifier triggered!")
# See http://www.outflux.net/blog/archives/2008/03/09/using-select-on-a-fifo/ self._notifier.setEnabled(False)
# We also use os.open and os.fdopen rather than built-in open so we for line in self.fifo:
# can add O_NONBLOCK. self.got_line.emit(line.rstrip('\r\n'))
fd = os.open(self._filepath, os.O_RDWR | self._notifier.setEnabled(True)
os.O_NONBLOCK) # pylint: disable=no-member
self.fifo = os.fdopen(fd, 'r') def cleanup(self):
except OSError: """Clean up so the fifo can be closed."""
log.procs.exception("Failed to read FIFO") self._notifier.setEnabled(False)
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):
@ -144,6 +122,7 @@ class _BaseUserscriptRunner(QObject):
def _cleanup(self): def _cleanup(self):
"""Clean up the temporary file.""" """Clean up the temporary file."""
log.procs.debug("Deleting temporary file {}.".format(self._filepath))
try: try:
os.remove(self._filepath) os.remove(self._filepath)
except OSError as e: except OSError as e:
@ -180,24 +159,22 @@ class _BaseUserscriptRunner(QObject):
# executed async. # executed async.
message.error(self._win_id, message.error(self._win_id,
"Error while calling userscript: {}".format(msg)) "Error while calling userscript: {}".format(msg))
log.procs.debug("Userscript process error: {} - {}".format(error, msg))
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,50 +182,41 @@ 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='qutebrowser-userscript-',
dir=rundir)
os.mkfifo(self._filepath) # pylint: disable=no-member os.mkfifo(self._filepath) # pylint: disable=no-member
except OSError as e: except OSError as e:
message.error(self._win_id, "Error while creating FIFO: {}".format( message.error(self._win_id, "Error while creating FIFO: {}".format(
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("Userscript process 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):
@ -281,7 +249,7 @@ class _WindowsUserscriptRunner(_BaseUserscriptRunner):
def on_proc_finished(self): def on_proc_finished(self):
"""Read back the commands when the process finished.""" """Read back the commands when the process finished."""
log.procs.debug("proc finished") log.procs.debug("Userscript process finished.")
try: try:
with open(self._filepath, 'r', encoding='utf-8') as f: with open(self._filepath, 'r', encoding='utf-8') as f:
for line in f: for line in f:
@ -346,6 +314,9 @@ def run(cmd, *args, url, win_id):
urlstr = url.toString(QUrl.FullyEncoded) urlstr = url.toString(QUrl.FullyEncoded)
commandrunner = runners.CommandRunner(win_id, tabbed_browser) commandrunner = runners.CommandRunner(win_id, tabbed_browser)
runner = UserscriptRunner(win_id, tabbed_browser) runner = UserscriptRunner(win_id, tabbed_browser)
runner.got_cmd.connect(
lambda cmd: log.commands.debug("Got userscript command: {}".format(
cmd)))
runner.got_cmd.connect(commandrunner.run_safely) runner.got_cmd.connect(commandrunner.run_safely)
runner.run(cmd, *args, env={'QUTE_URL': urlstr}) runner.run(cmd, *args, env={'QUTE_URL': urlstr})
runner.finished.connect(commandrunner.deleteLater) runner.finished.connect(commandrunner.deleteLater)