qutebrowser/qutebrowser/misc/ipc.py

331 lines
11 KiB
Python
Raw Normal View History

2014-10-13 07:06:57 +02:00
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
2015-01-03 15:51:31 +01:00
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
2014-10-13 07:06:57 +02:00
#
# 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."""
2014-11-30 19:22:35 +01:00
import os
2014-10-13 07:06:57 +02:00
import json
import getpass
2014-10-13 22:51:11 +02:00
import binascii
import hashlib
2014-10-13 07:06:57 +02:00
2015-05-01 14:44:41 +02:00
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
from PyQt5.QtNetwork import QLocalSocket, QLocalServer, QAbstractSocket
2014-10-13 07:06:57 +02:00
from qutebrowser.utils import log, usertypes, error, objreg
2014-10-13 07:06:57 +02:00
CONNECT_TIMEOUT = 100
WRITE_TIMEOUT = 1000
2014-10-13 22:47:32 +02:00
READ_TIMEOUT = 5000
2014-10-13 07:06:57 +02:00
def _get_socketname(basedir, user=None):
"""Get a socketname to use."""
if user is None:
user = getpass.getuser()
parts = ['qutebrowser', user]
if basedir is not None:
md5 = hashlib.md5(basedir.encode('utf-8'))
parts.append(md5.hexdigest())
return '-'.join(parts)
class Error(Exception):
2014-10-13 07:11:45 +02:00
"""Exception raised when there was a problem with IPC."""
class ListenError(Error):
"""Exception raised when there was a problem with listening to IPC.
Args:
code: The error code.
message: The error message.
"""
def __init__(self, server):
"""Constructor.
Args:
server: The QLocalServer which has the error set.
"""
super().__init__()
self.code = server.serverError()
self.message = server.errorString()
def __str__(self):
return "Error while listening to IPC server: {} (error {})".format(
self.message, self.code)
class AddressInUseError(ListenError):
"""Emitted when the server address is already in use."""
class IPCServer(QObject):
2014-10-13 22:48:37 +02:00
"""IPC server to which clients connect to.
Attributes:
ignored: Whether requests are ignored (in exception hook).
2014-10-13 22:48:37 +02:00
_timer: A timer to handle timeouts.
_server: A QLocalServer to accept new connections.
_socket: The QLocalSocket we're currently connected to.
_socketname: The socketname to use.
2015-05-01 14:44:41 +02:00
Signals:
got_args: Emitted when there was an IPC connection and arguments were
passed.
2014-10-13 22:48:37 +02:00
"""
2015-05-01 14:44:41 +02:00
got_args = pyqtSignal(list, str)
def __init__(self, args, parent=None):
"""Start the IPC server and listen to commands.
Args:
args: The argparse namespace.
parent: The parent to be used.
"""
super().__init__(parent)
self.ignored = False
self._socketname = _get_socketname(args.basedir)
2014-10-13 22:47:32 +02:00
self._timer = usertypes.Timer(self, 'ipc-timeout')
self._timer.setInterval(READ_TIMEOUT)
self._timer.timeout.connect(self.on_timeout)
self._server = QLocalServer(self)
2014-10-13 21:12:15 +02:00
self._server.newConnection.connect(self.handle_connection)
self._socket = None
2014-10-13 20:36:23 +02:00
def _remove_server(self):
"""Remove an existing server."""
ok = QLocalServer.removeServer(self._socketname)
2014-10-13 20:36:23 +02:00
if not ok:
raise Error("Error while removing server {}!".format(
self._socketname))
2014-10-13 20:36:23 +02:00
def listen(self):
"""Start listening on self._servername."""
log.ipc.debug("Listening as {}".format(self._socketname))
self._remove_server()
ok = self._server.listen(self._socketname)
if not ok:
if self._server.serverError() == QAbstractSocket.AddressInUseError:
raise AddressInUseError(self._server)
else:
raise ListenError(self._server)
@pyqtSlot(int)
def on_error(self, err):
"""Convenience method which calls _socket_error on an error."""
2014-10-13 22:47:32 +02:00
self._timer.stop()
2014-10-13 22:38:40 +02:00
log.ipc.debug("Socket error {}: {}".format(
self._socket.error(), self._socket.errorString()))
if err != QLocalSocket.PeerClosedError:
_socket_error("handling IPC connection", self._socket)
@pyqtSlot()
2014-10-13 21:12:15 +02:00
def handle_connection(self):
"""Handle a new connection to the server."""
if self.ignored:
return
if self._socket is not None:
2014-10-13 21:38:28 +02:00
log.ipc.debug("Got new connection but ignoring it because we're "
"still handling another one.")
2014-10-13 21:12:15 +02:00
return
socket = self._server.nextPendingConnection()
if socket is None:
2014-10-13 21:38:28 +02:00
log.ipc.debug("No new connection to handle.")
2014-10-13 21:12:15 +02:00
return
2014-10-13 21:38:28 +02:00
log.ipc.debug("Client connected.")
2014-10-13 22:47:32 +02:00
self._timer.start()
self._socket = socket
2014-10-13 21:12:15 +02:00
socket.readyRead.connect(self.on_ready_read)
if socket.canReadLine():
log.ipc.debug("We can read a line immediately.")
self.on_ready_read()
2014-10-13 21:12:15 +02:00
socket.error.connect(self.on_error)
2014-10-13 22:36:05 +02:00
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()
2014-10-13 20:37:09 +02:00
@pyqtSlot()
def on_disconnected(self):
"""Clean up socket when the client disconnected."""
2014-10-13 21:38:28 +02:00
log.ipc.debug("Client disconnected.")
2014-10-13 22:47:32 +02:00
self._timer.stop()
self._socket.deleteLater()
self._socket = None
2014-10-13 21:12:15 +02:00
# Maybe another connection is waiting.
self.handle_connection()
2014-10-13 20:37:09 +02:00
@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
2014-10-13 22:47:32 +02:00
self._timer.start()
while self._socket is not None and self._socket.canReadLine():
data = bytes(self._socket.readLine())
2014-10-13 21:38:28 +02:00
log.ipc.debug("Read from socket: {}".format(data))
2014-10-13 21:17:49 +02:00
try:
decoded = data.decode('utf-8')
except UnicodeDecodeError:
2014-10-13 22:51:11 +02:00
log.ipc.error("Ignoring invalid IPC data.")
2014-10-14 07:58:50 +02:00
log.ipc.debug("invalid data: {}".format(
binascii.hexlify(data)))
2014-10-13 21:17:49 +02:00
return
2014-11-30 19:22:35 +01:00
log.ipc.debug("Processing: {}".format(decoded))
2014-10-13 21:16:38 +02:00
try:
2014-11-30 19:22:35 +01:00
json_data = json.loads(decoded)
2014-10-13 21:16:38 +02:00
except ValueError:
2014-10-13 22:51:11 +02:00
log.ipc.error("Ignoring invalid IPC data.")
log.ipc.debug("invalid json: {}".format(decoded.strip()))
2014-10-13 21:16:38 +02:00
return
2014-11-30 19:22:35 +01:00
try:
args = json_data['args']
except KeyError:
log.ipc.error("Ignoring invalid IPC data.")
log.ipc.debug("no args: {}".format(decoded.strip()))
2014-11-30 19:22:35 +01:00
return
cwd = json_data.get('cwd', None)
2015-05-01 14:44:41 +02:00
self.got_args.emit(args, cwd)
2014-10-13 22:47:32 +02:00
@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()
2014-10-13 20:36:23 +02:00
def shutdown(self):
"""Shut down the IPC server cleanly."""
if self._socket is not None:
self._socket.deleteLater()
self._socket = None
2014-10-13 22:47:32 +02:00
self._timer.stop()
2014-10-13 20:36:23 +02:00
self._server.close()
self._server.deleteLater()
self._remove_server()
2014-10-13 20:38:40 +02:00
2014-10-13 07:47:05 +02:00
def _socket_error(action, socket):
"""Raise an Error based on an action and a QLocalSocket.
2014-10-13 07:47:05 +02:00
Args:
action: A string like "writing to running instance".
2014-10-13 20:36:41 +02:00
socket: A QLocalSocket.
2014-10-13 07:47:05 +02:00
"""
raise Error("Error while {}: {} (error {})".format(
2014-10-13 07:47:05 +02:00
action, socket.errorString(), socket.error()))
def send_to_running_instance(args):
2014-10-13 07:06:57 +02:00
"""Try to send a commandline to a running instance.
Blocks for CONNECT_TIMEOUT ms.
Args:
args: The argparse namespace.
2014-10-13 07:06:57 +02:00
Return:
True if connecting was successful, False if no connection was made.
"""
socket = QLocalSocket()
socketname = _get_socketname(args.basedir)
2015-08-24 19:28:09 +02:00
log.ipc.debug("Connecting to {}".format(socketname))
socket.connectToServer(socketname)
2014-10-13 07:06:57 +02:00
connected = socket.waitForConnected(100)
if connected:
2014-10-13 21:38:28 +02:00
log.ipc.info("Opening in existing instance")
json_data = {'args': args.command}
try:
cwd = os.getcwd()
except OSError:
pass
else:
json_data['cwd'] = cwd
2014-11-30 19:22:35 +01:00
line = json.dumps(json_data) + '\n'
2014-10-13 21:38:28 +02:00
data = line.encode('utf-8')
log.ipc.debug("Writing: {}".format(data))
socket.writeData(data)
socket.waitForBytesWritten(WRITE_TIMEOUT)
if socket.error() != QLocalSocket.UnknownSocketError:
2014-10-13 07:47:05 +02:00
_socket_error("writing to running instance", socket)
else:
return True
2014-10-13 07:06:57 +02:00
else:
if socket.error() not in (QLocalSocket.ConnectionRefusedError,
QLocalSocket.ServerNotFoundError):
2014-10-13 07:47:05 +02:00
_socket_error("connecting to running instance", socket)
else:
2014-10-13 21:38:28 +02:00
log.ipc.debug("No existing instance present (error {})".format(
socket.error()))
2014-10-13 07:47:05 +02:00
return False
def display_error(exc, args):
"""Display a message box with an IPC error."""
error.handle_fatal_exc(
exc, args, "Error while connecting to running instance!",
post_text="Maybe another instance is running but frozen?")
def send_or_listen(args):
"""Send the args to a running instance or start a new IPCServer.
Args:
args: The argparse namespace.
Return:
The IPCServer instance if no running instance was detected.
None if an instance was running and received our request.
"""
try:
sent = send_to_running_instance(args)
if sent:
return None
log.init.debug("Starting IPC server...")
server = IPCServer(args)
server.listen()
objreg.register('ipc-server', server)
return server
except AddressInUseError as e:
# This could be a race condition...
log.init.debug("Got AddressInUseError, trying again.")
time.sleep(500)
sent = send_to_running_instance(args)
if sent:
return None
else:
display_error(e, args)
raise
except Error as e:
display_error(e, args)
raise