Cleanup and documentation in commands/userscripts
This commit is contained in:
parent
8d570b686c
commit
22072eac7d
@ -32,22 +32,39 @@ from qutebrowser.utils.misc import get_standard_dir
|
||||
|
||||
class _BlockingFIFOReader(QObject):
|
||||
|
||||
"""A worker which reads commands from a FIFO endlessly."""
|
||||
"""A worker which reads commands from a FIFO endlessly.
|
||||
|
||||
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)
|
||||
finished = pyqtSignal()
|
||||
|
||||
def __init__(self, filename):
|
||||
def __init__(self, filepath):
|
||||
super().__init__()
|
||||
self.filename = filename
|
||||
self.filepath = filepath
|
||||
self.fifo = None
|
||||
|
||||
def read(self):
|
||||
"""Blocking read loop which emits got_line when a new line arrived."""
|
||||
# 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(self.filename, os.O_RDWR | os.O_NONBLOCK)
|
||||
fd = os.open(self.filepath, os.O_RDWR | os.O_NONBLOCK)
|
||||
self.fifo = os.fdopen(fd, 'r')
|
||||
while True:
|
||||
logging.debug("thread loop")
|
||||
@ -62,7 +79,21 @@ class _BlockingFIFOReader(QObject):
|
||||
return
|
||||
|
||||
|
||||
class _AbstractUserscriptRunner(QObject):
|
||||
class _BaseUserscriptRunner(QObject):
|
||||
|
||||
"""Common part between the Windows and the POSIX userscript runners.
|
||||
|
||||
Attributes:
|
||||
filepath: The path of the file/FIFO which is being read.
|
||||
proc: The QProcess which is being executed.
|
||||
|
||||
Class attributes:
|
||||
PROCESS_MESSAGES: A mapping of QProcess::ProcessError members to
|
||||
human-readable error strings.
|
||||
|
||||
Signals:
|
||||
got_cmd: Emitted when a new command arrived and should be executed.
|
||||
"""
|
||||
|
||||
got_cmd = pyqtSignal(str)
|
||||
|
||||
@ -83,6 +114,13 @@ class _AbstractUserscriptRunner(QObject):
|
||||
self.proc = None
|
||||
|
||||
def _run_process(self, cmd, *args, env):
|
||||
"""Start the given command via QProcess.
|
||||
|
||||
Args:
|
||||
cmd: The command to be started.
|
||||
*args: The arguments to hand to the command
|
||||
env: A dictionary of environment variables to add.
|
||||
"""
|
||||
self.proc = QProcess()
|
||||
procenv = QProcessEnvironment.systemEnvironment()
|
||||
procenv.insert('QUTE_FIFO', self.filepath)
|
||||
@ -95,29 +133,55 @@ class _AbstractUserscriptRunner(QObject):
|
||||
self.proc.start(cmd, args)
|
||||
|
||||
def _cleanup(self):
|
||||
"""Clean up the temporary file."""
|
||||
try:
|
||||
os.remove(self.filepath)
|
||||
except PermissionError:
|
||||
message.error("Failed to delete tempfile...")
|
||||
self.filepath = None
|
||||
self.proc = None
|
||||
|
||||
def run(self, cmd, *args, env=None):
|
||||
"""Run the userscript given.
|
||||
|
||||
Needs to be overridden by superclasses.
|
||||
|
||||
Args:
|
||||
cmd: The command to be started.
|
||||
*args: The arguments to hand to the command
|
||||
env: A dictionary of environment variables to add.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def on_proc_finished(self):
|
||||
"""Called when the process has finished.
|
||||
|
||||
Needs to be overridden by superclasses.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def on_proc_error(self, error):
|
||||
"""Called when the process encountered an error."""
|
||||
msg = self.PROCESS_MESSAGES[error]
|
||||
message.error("Error while calling userscript: {}".format(msg))
|
||||
|
||||
|
||||
class _POSIXUserscriptRunner(_AbstractUserscriptRunner):
|
||||
class _POSIXUserscriptRunner(_BaseUserscriptRunner):
|
||||
|
||||
"""Userscript runner to be used on POSIX. Uses _BlockingFIFOReader.
|
||||
|
||||
The OS must have support for named pipes and select(). Commands are
|
||||
executed immediately when they arrive in the FIFO.
|
||||
|
||||
Attributes:
|
||||
reader: The _BlockingFIFOReader instance.
|
||||
thread: The QThread where reader runs.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.reader = None
|
||||
self.thread = None
|
||||
self.proc = None
|
||||
|
||||
def run(self, cmd, *args, env=None):
|
||||
rundir = get_standard_dir(QStandardPaths.RuntimeLocation)
|
||||
@ -141,14 +205,17 @@ class _POSIXUserscriptRunner(_AbstractUserscriptRunner):
|
||||
self.thread.start()
|
||||
|
||||
def on_proc_finished(self):
|
||||
"""Interrupt the reader when the process finished."""
|
||||
logging.debug("proc finished")
|
||||
self.thread.requestInterruption()
|
||||
|
||||
def on_proc_error(self, error):
|
||||
"""Interrupt the reader when the process had an error."""
|
||||
super().on_proc_error(error)
|
||||
self.thread.requestInterruption()
|
||||
|
||||
def on_reader_finished(self):
|
||||
"""Quit the thread and clean up when the reader finished."""
|
||||
logging.debug("reader finished")
|
||||
self.thread.quit()
|
||||
self.reader.fifo.close()
|
||||
@ -156,25 +223,40 @@ class _POSIXUserscriptRunner(_AbstractUserscriptRunner):
|
||||
super()._cleanup()
|
||||
|
||||
def on_thread_finished(self):
|
||||
"""Clean up the QThread object when the thread finished."""
|
||||
logging.debug("thread finished")
|
||||
self.thread.deleteLater()
|
||||
|
||||
|
||||
class _WindowsUserscriptRunner(_AbstractUserscriptRunner):
|
||||
class _WindowsUserscriptRunner(_BaseUserscriptRunner):
|
||||
|
||||
"""Userscript runner to be used on Windows.
|
||||
|
||||
This is a much more dumb implementation compared to POSIXUserscriptRunner.
|
||||
It uses a normal flat file for commands and executes them all at once when
|
||||
the process has finished, as Windows doesn't really understand the concept
|
||||
of using files as named pipes.
|
||||
|
||||
This also means the userscript *has* to use >> (append) rather than >
|
||||
(overwrite) to write to the file!
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.oshandle = None
|
||||
self.proc = None
|
||||
|
||||
def _cleanup(self):
|
||||
"""Clean up temporary files after the userscript finished."""
|
||||
os.close(self.oshandle)
|
||||
super()._cleanup()
|
||||
self.oshandle = None
|
||||
self.proc = None
|
||||
|
||||
def on_proc_finished(self):
|
||||
"""Read back the commands when the process finished.
|
||||
|
||||
Emit:
|
||||
got_cmd: Emitted for every command in the file.
|
||||
"""
|
||||
logging.debug("proc finished")
|
||||
with open(self.filepath, 'r') as f:
|
||||
for line in f:
|
||||
@ -182,6 +264,7 @@ class _WindowsUserscriptRunner(_AbstractUserscriptRunner):
|
||||
self._cleanup()
|
||||
|
||||
def on_proc_error(self, error):
|
||||
"""Clean up when the process had an error."""
|
||||
super().on_proc_error(error)
|
||||
self._cleanup()
|
||||
|
||||
@ -192,23 +275,21 @@ class _WindowsUserscriptRunner(_AbstractUserscriptRunner):
|
||||
|
||||
class _DummyUserscriptRunner:
|
||||
|
||||
"""Simple dummy runner which displays an error when using userscripts.
|
||||
|
||||
Used on unknown systems since we don't know what (or if any) approach will
|
||||
work there.
|
||||
"""
|
||||
|
||||
def run(self, _cmd, *_args, _env=None):
|
||||
message.error("Userscripts are not supported on this platform!")
|
||||
|
||||
|
||||
class UserscriptRunner(QObject):
|
||||
|
||||
got_cmd = pyqtSignal(str)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
if os.name == 'posix':
|
||||
self.runner = _POSIXUserscriptRunner()
|
||||
elif os.name == 'nt':
|
||||
self.runner = _WindowsUserscriptRunner()
|
||||
else:
|
||||
self.runner = _DummyUserscriptRunner()
|
||||
self.runner.got_cmd.connect(self.got_cmd)
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
return self.runner.run(*args, **kwargs)
|
||||
# Here we basically just assign a generic UserscriptRunner class which does the
|
||||
# right thing depending on the platform.
|
||||
if os.name == 'posix':
|
||||
UserscriptRunner = _POSIXUserscriptRunner
|
||||
elif os.name == 'nt':
|
||||
UserscriptRunner = _WindowsUserscriptRunner
|
||||
else:
|
||||
UserscriptRunner = _DummyUserscriptRunner
|
||||
|
Loading…
Reference in New Issue
Block a user