Merge branch 'ipc'

This commit is contained in:
Florian Bruhin 2014-10-13 23:19:04 +02:00
commit ca85dde71f
3 changed files with 243 additions and 5 deletions

View File

@ -43,7 +43,8 @@ from qutebrowser.browser import quickmarks, cookies, downloads, cache
from qutebrowser.widgets import mainwindow, crash from qutebrowser.widgets import mainwindow, crash
from qutebrowser.keyinput import modeman from qutebrowser.keyinput import modeman
from qutebrowser.utils import (log, version, message, readline, utils, qtutils, from qutebrowser.utils import (log, version, message, readline, utils, qtutils,
urlutils, debug, objreg, usertypes, standarddir) urlutils, debug, objreg, usertypes, standarddir,
ipc)
# We import utilcmds to run the cmdutils.register decorators. # We import utilcmds to run the cmdutils.register decorators.
from qutebrowser.utils import utilcmds # pylint: disable=unused-import from qutebrowser.utils import utilcmds # pylint: disable=unused-import
@ -102,6 +103,10 @@ class Application(QApplication):
print(version.GPL_BOILERPLATE.strip()) print(version.GPL_BOILERPLATE.strip())
sys.exit(0) sys.exit(0)
sent = ipc.send_to_running_instance(self._args.command)
if sent:
sys.exit(0)
log.init.debug("Starting init...") log.init.debug("Starting init...")
self.setQuitOnLastWindowClosed(False) self.setQuitOnLastWindowClosed(False)
self.setOrganizationName("qutebrowser") self.setOrganizationName("qutebrowser")
@ -121,6 +126,9 @@ class Application(QApplication):
log.init.debug("Applying python hacks...") log.init.debug("Applying python hacks...")
self._python_hacks() self._python_hacks()
log.init.debug("Starting IPC server...")
ipc.init()
log.init.debug("Init done!") log.init.debug("Init done!")
if self._crashdlg is not None: if self._crashdlg is not None:
@ -218,17 +226,24 @@ class Application(QApplication):
def _open_pages(self): def _open_pages(self):
"""Open startpage etc. and process commandline args.""" """Open startpage etc. and process commandline args."""
self._process_init_args() self.process_args(self._args.command)
self._open_startpage() self._open_startpage()
self._open_quickstart() self._open_quickstart()
def _process_init_args(self): def process_args(self, args, ipc=False):
"""Process commandline args. """Process commandline args.
URLs to open have no prefix, commands to execute begin with a colon. URLs to open have no prefix, commands to execute begin with a colon.
Args:
args: A list of arguments to process.
ipc: Whether the arguments were transmitted over IPC.
""" """
if ipc:
win_id = mainwindow.MainWindow.spawn()
else:
win_id = 0 win_id = 0
for cmd in self._args.command: for cmd in args:
if cmd.startswith(':'): if cmd.startswith(':'):
log.init.debug("Startup cmd {}".format(cmd)) log.init.debug("Startup cmd {}".format(cmd))
commandrunner = runners.CommandRunner(win_id) commandrunner = runners.CommandRunner(win_id)
@ -621,6 +636,11 @@ class Application(QApplication):
tabbed_browser = objreg.get('tabbed-browser', scope='window', tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id) window=win_id)
tabbed_browser.shutdown() tabbed_browser.shutdown()
# Shut down IPC
try:
objreg.get('ipc-server').shutdown()
except KeyError:
pass
# Save everything # Save everything
try: try:
config_obj = objreg.get('config') config_obj = objreg.get('config')

217
qutebrowser/utils/ipc.py Normal file
View File

@ -0,0 +1,217 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# 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/>.
"""Utilities for IPC with existing instances."""
import json
import getpass
import binascii
from PyQt5.QtCore import pyqtSlot, QObject
from PyQt5.QtNetwork import QLocalSocket, QLocalServer
from qutebrowser.utils import log, objreg, usertypes
SOCKETNAME = 'qutebrowser-{}'.format(getpass.getuser())
CONNECT_TIMEOUT = 100
WRITE_TIMEOUT = 1000
READ_TIMEOUT = 5000
class IPCError(Exception):
"""Exception raised when there was a problem with IPC."""
class IPCServer(QObject):
"""IPC server to which clients connect to.
Attributes:
_timer: A timer to handle timeouts.
_server: A QLocalServer to accept new connections.
_socket: The QLocalSocket we're currently connected to.
"""
def __init__(self, parent=None):
"""Start the IPC server and listen to commands."""
super().__init__(parent)
self._remove_server()
self._timer = usertypes.Timer(self, 'ipc-timeout')
self._timer.setInterval(READ_TIMEOUT)
self._timer.timeout.connect(self.on_timeout)
self._server = QLocalServer(self)
ok = self._server.listen(SOCKETNAME)
if not ok:
raise IPCError("Error while listening to IPC server: {} "
"(error {})".format(self._server.errorString(),
self._server.serverError()))
self._server.newConnection.connect(self.handle_connection)
self._socket = None
def _remove_server(self):
"""Remove an existing server."""
ok = QLocalServer.removeServer(SOCKETNAME)
if not ok:
raise IPCError("Error while removing server {}!".format(
SOCKETNAME))
@pyqtSlot(int)
def on_error(self, error):
"""Convenience method which calls _socket_error on an error."""
self._timer.stop()
log.ipc.debug("Socket error {}: {}".format(
self._socket.error(), self._socket.errorString()))
if error != QLocalSocket.PeerClosedError:
_socket_error("handling IPC connection", self._socket)
@pyqtSlot()
def handle_connection(self):
"""Handle a new connection to the server."""
if self._socket is not None:
log.ipc.debug("Got new connection but ignoring it because we're "
"still handling another one.")
return
socket = self._server.nextPendingConnection()
if socket is None:
log.ipc.debug("No new connection to handle.")
return
log.ipc.debug("Client connected.")
self._timer.start()
self._socket = socket
socket.readyRead.connect(self.on_ready_read)
if socket.canReadLine():
log.ipc.debug("We can read a line immediately.")
self.on_ready_read()
socket.error.connect(self.on_error)
if socket.error() not in (QLocalSocket.UnknownSocketError,
QLocalSocket.PeerClosedError):
log.ipc.debug("We got an error immediately.")
self.on_error(socket.error())
socket.disconnected.connect(self.on_disconnected)
if socket.state() == QLocalSocket.UnconnectedState:
log.ipc.debug("Socket was disconnected immediately.")
self.on_disconnected()
@pyqtSlot()
def on_disconnected(self):
"""Clean up socket when the client disconnected."""
log.ipc.debug("Client disconnected.")
self._timer.stop()
self._socket.deleteLater()
self._socket = None
# Maybe another connection is waiting.
self.handle_connection()
@pyqtSlot()
def on_ready_read(self):
"""Read json data from the client."""
if self._socket is None:
# this happened once and I don't know why
log.ipc.warn("In on_ready_read with None socket!")
return
self._timer.start()
while self._socket.canReadLine():
data = bytes(self._socket.readLine())
log.ipc.debug("Read from socket: {}".format(data))
try:
decoded = data.decode('utf-8')
except UnicodeDecodeError:
log.ipc.error("Ignoring invalid IPC data.")
log.ipc.debug("invalid data: {}".format(binascii.hexlify(data)))
return
try:
args = json.loads(decoded)
except ValueError:
log.ipc.error("Ignoring invalid IPC data.")
log.ipc.debug("invalid json: {}".format(decoded.strip()))
return
log.ipc.debug("Processing: {}".format(decoded))
app = objreg.get('app')
app.process_args(args, ipc=True)
@pyqtSlot()
def on_timeout(self):
"""Cancel the current connection if it was idle for too long."""
log.ipc.error("IPC connection timed out.")
self._socket.close()
def shutdown(self):
"""Shut down the IPC server cleanly."""
if self._socket is not None:
self._socket.deleteLater()
self._socket = None
self._timer.stop()
self._server.close()
self._server.deleteLater()
self._remove_server()
def init():
"""Initialize the global IPC server."""
app = objreg.get('app')
server = IPCServer(app)
objreg.register('ipc-server', server)
def _socket_error(action, socket):
"""Raise an IPCError based on an action and a QLocalSocket.
Args:
action: A string like "writing to running instance".
socket: A QLocalSocket.
"""
raise IPCError("Error while {}: {} (error {})".format(
action, socket.errorString(), socket.error()))
def send_to_running_instance(cmdlist):
"""Try to send a commandline to a running instance.
Blocks for CONNECT_TIMEOUT ms.
Args:
cmdlist: A list to send (URLs/commands)
Return:
True if connecting was successful, False if no connection was made.
"""
socket = QLocalSocket()
socket.connectToServer(SOCKETNAME)
connected = socket.waitForConnected(100)
if connected:
log.ipc.info("Opening in existing instance")
line = json.dumps(cmdlist) + '\n'
data = line.encode('utf-8')
log.ipc.debug("Writing: {}".format(data))
socket.writeData(data)
socket.waitForBytesWritten(WRITE_TIMEOUT)
if socket.error() != QLocalSocket.UnknownSocketError:
_socket_error("writing to running instance", socket)
else:
return True
else:
if socket.error() not in (QLocalSocket.ConnectionRefusedError,
QLocalSocket.ServerNotFoundError):
_socket_error("connecting to running instance", socket)
else:
log.ipc.debug("No existing instance present (error {})".format(
socket.error()))
return False

View File

@ -124,6 +124,7 @@ js = logging.getLogger('js') # Javascript console messages
qt = logging.getLogger('qt') # Warnings produced by Qt qt = logging.getLogger('qt') # Warnings produced by Qt
style = logging.getLogger('style') style = logging.getLogger('style')
rfc6266 = logging.getLogger('rfc6266') rfc6266 = logging.getLogger('rfc6266')
ipc = logging.getLogger('ipc')
ram_handler = None ram_handler = None