Initial userscript support

This commit is contained in:
Florian Bruhin 2014-05-21 19:53:58 +02:00
parent ecc838d02c
commit 8d570b686c
4 changed files with 228 additions and 0 deletions

View File

@ -382,6 +382,7 @@ class QuteBrowser(QApplication):
cmd.returnPressed.connect(tabs.setFocus) cmd.returnPressed.connect(tabs.setFocus)
self.searchmanager.do_search.connect(tabs.search) self.searchmanager.do_search.connect(tabs.search)
kp['normal'].keystring_updated.connect(status.keystring.setText) kp['normal'].keystring_updated.connect(status.keystring.setText)
tabs.got_cmd.connect(self.commandmanager.run_safely)
# hints # hints
kp['hint'].fire_hint.connect(tabs.fire_hint) kp['hint'].fire_hint.connect(tabs.fire_hint)

View File

@ -36,6 +36,7 @@ import qutebrowser.utils.webelem as webelem
from qutebrowser.utils.misc import check_overflow, shell_escape from qutebrowser.utils.misc import check_overflow, shell_escape
from qutebrowser.utils.editor import ExternalEditor from qutebrowser.utils.editor import ExternalEditor
from qutebrowser.commands.exceptions import CommandError from qutebrowser.commands.exceptions import CommandError
from qutebrowser.commands.userscripts import UserscriptRunner
class CommandDispatcher(QObject): class CommandDispatcher(QObject):
@ -51,6 +52,7 @@ class CommandDispatcher(QObject):
Attributes: Attributes:
_tabs: The TabbedBrowser object. _tabs: The TabbedBrowser object.
_editor: The ExternalEditor object. _editor: The ExternalEditor object.
_userscript_runners: A list of userscript runners.
""" """
def __init__(self, parent): def __init__(self, parent):
@ -60,6 +62,7 @@ class CommandDispatcher(QObject):
parent: The TabbedBrowser for this dispatcher. parent: The TabbedBrowser for this dispatcher.
""" """
super().__init__(parent) super().__init__(parent)
self._userscript_runners = []
self._tabs = parent self._tabs = parent
self._editor = None self._editor = None
@ -589,6 +592,15 @@ class CommandDispatcher(QObject):
"""Open main startpage in current tab.""" """Open main startpage in current tab."""
self.openurl(config.get('general', 'startpage')[0]) self.openurl(config.get('general', 'startpage')[0])
@cmdutils.register(instance='mainwindow.tabs.cmd')
def run_userscript(self, cmd, *args):
"""Run an userscript given as argument."""
url = urlutils.urlstring(self._tabs.currentWidget().url())
runner = UserscriptRunner()
runner.got_cmd.connect(self._tabs.got_cmd)
runner.run(cmd, *args, env={'QUTE_URL': url})
self._userscript_runners.append(runner)
@cmdutils.register(instance='mainwindow.tabs.cmd', modes=['insert'], @cmdutils.register(instance='mainwindow.tabs.cmd', modes=['insert'],
hide=True) hide=True)
def open_editor(self): def open_editor(self):

View File

@ -0,0 +1,214 @@
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Functions to execute an userscript."""
import os
import os.path
import logging
import tempfile
from select import select
from PyQt5.QtCore import (pyqtSignal, QObject, QThread, QStandardPaths,
QProcessEnvironment, QProcess)
import qutebrowser.utils.message as message
from qutebrowser.utils.misc import get_standard_dir
class _BlockingFIFOReader(QObject):
"""A worker which reads commands from a FIFO endlessly."""
got_line = pyqtSignal(str)
finished = pyqtSignal()
def __init__(self, filename):
super().__init__()
self.filename = filename
self.fifo = None
def read(self):
# 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)
self.fifo = os.fdopen(fd, 'r')
while True:
logging.debug("thread loop")
ready_r, _ready_w, _ready_e = select([self.fifo], [], [], 1)
if ready_r:
logging.debug("reading data")
for line in self.fifo:
self.got_line.emit(line.rstrip())
if QThread.currentThread().isInterruptionRequested():
# FIXME this only exists since Qt 5.2, is that an issue?
self.finished.emit()
return
class _AbstractUserscriptRunner(QObject):
got_cmd = pyqtSignal(str)
PROCESS_MESSAGES = {
QProcess.FailedToStart: "The process failed to start.",
QProcess.Crashed: "The process crashed.",
QProcess.Timedout: "The last waitFor...() function timed out.",
QProcess.WriteError: ("An error occurred when attempting to write to "
"the process."),
QProcess.ReadError: ("An error occurred when attempting to read from "
"the process."),
QProcess.UnknownError: "An unknown error occurred.",
}
def __init__(self):
super().__init__()
self.filepath = None
self.proc = None
def _run_process(self, cmd, *args, env):
self.proc = QProcess()
procenv = QProcessEnvironment.systemEnvironment()
procenv.insert('QUTE_FIFO', self.filepath)
if env is not None:
for k, v in env.items():
procenv.insert(k, v)
self.proc.setProcessEnvironment(procenv)
self.proc.error.connect(self.on_proc_error)
self.proc.finished.connect(self.on_proc_finished)
self.proc.start(cmd, args)
def _cleanup(self):
try:
os.remove(self.filepath)
except PermissionError:
message.error("Failed to delete tempfile...")
def run(self, cmd, *args, env=None):
raise NotImplementedError
def on_proc_finished(self):
raise NotImplementedError
def on_proc_error(self, error):
msg = self.PROCESS_MESSAGES[error]
message.error("Error while calling userscript: {}".format(msg))
class _POSIXUserscriptRunner(_AbstractUserscriptRunner):
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)
# tempfile.mktemp is deprecated and discouraged, but we use it here to
# create a FIFO since the only other alternative would be to create a
# directory and place the FIFO there, which sucks. Since os.kfifo will
# raise an exception anyways when the path doesn't exist, it shouldn't
# be a big issue.
self.filepath = tempfile.mktemp(prefix='userscript-', dir=rundir)
os.mkfifo(self.filepath)
self.reader = _BlockingFIFOReader(self.filepath)
self.thread = QThread()
self.reader.moveToThread(self.thread)
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.thread.start()
def on_proc_finished(self):
logging.debug("proc finished")
self.thread.requestInterruption()
def on_proc_error(self, error):
super().on_proc_error(error)
self.thread.requestInterruption()
def on_reader_finished(self):
logging.debug("reader finished")
self.thread.quit()
self.reader.fifo.close()
self.reader.deleteLater()
super()._cleanup()
def on_thread_finished(self):
logging.debug("thread finished")
self.thread.deleteLater()
class _WindowsUserscriptRunner(_AbstractUserscriptRunner):
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):
logging.debug("proc finished")
with open(self.filepath, 'r') as f:
for line in f:
self.got_cmd.emit(line.rstrip())
self._cleanup()
def on_proc_error(self, error):
super().on_proc_error(error)
self._cleanup()
def run(self, cmd, *args, env=None):
self.oshandle, self.filepath = tempfile.mkstemp(text=True)
self._run_process(cmd, *args, env=env)
class _DummyUserscriptRunner:
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)

View File

@ -85,6 +85,7 @@ class TabbedBrowser(TabWidget):
shutdown_complete = pyqtSignal() shutdown_complete = pyqtSignal()
quit = pyqtSignal() quit = pyqtSignal()
resized = pyqtSignal('QRect') resized = pyqtSignal('QRect')
got_cmd = pyqtSignal(str)
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)