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
|
2015-05-16 23:10:20 +02:00
|
|
|
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
|
2015-04-17 19:22:56 +02:00
|
|
|
from PyQt5.QtNetwork import QLocalSocket, QLocalServer, QAbstractSocket
|
|
|
|
from PyQt5.QtWidgets import QMessageBox
|
2014-10-13 07:06:57 +02:00
|
|
|
|
2015-05-01 14:44:41 +02:00
|
|
|
from qutebrowser.utils import log, usertypes
|
2014-10-13 07:06:57 +02:00
|
|
|
|
|
|
|
|
|
|
|
CONNECT_TIMEOUT = 100
|
2014-10-13 07:34:15 +02:00
|
|
|
WRITE_TIMEOUT = 1000
|
2014-10-13 22:47:32 +02:00
|
|
|
READ_TIMEOUT = 5000
|
2014-10-13 07:06:57 +02:00
|
|
|
|
|
|
|
|
2015-05-16 23:10:20 +02:00
|
|
|
def _get_socketname(args):
|
|
|
|
"""Get a socketname to use."""
|
|
|
|
parts = ['qutebrowser', getpass.getuser()]
|
|
|
|
if args.basedir is not None:
|
|
|
|
md5 = hashlib.md5(args.basedir.encode('utf-8'))
|
|
|
|
parts.append(md5.hexdigest())
|
|
|
|
return '-'.join(parts)
|
|
|
|
|
|
|
|
|
2015-04-17 19:22:56 +02:00
|
|
|
class Error(Exception):
|
2014-10-13 07:11:45 +02:00
|
|
|
|
|
|
|
"""Exception raised when there was a problem with IPC."""
|
|
|
|
|
|
|
|
|
2015-04-17 19:22:56 +02:00
|
|
|
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."""
|
|
|
|
|
|
|
|
|
2014-10-13 20:11:13 +02:00
|
|
|
class IPCServer(QObject):
|
|
|
|
|
2014-10-13 22:48:37 +02:00
|
|
|
"""IPC server to which clients connect to.
|
|
|
|
|
|
|
|
Attributes:
|
2014-11-30 22:30:26 +01:00
|
|
|
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.
|
2015-05-16 23:10:20 +02:00
|
|
|
_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
|
|
|
"""
|
2014-10-13 20:11:13 +02:00
|
|
|
|
2015-05-01 14:44:41 +02:00
|
|
|
got_args = pyqtSignal(list, str)
|
|
|
|
|
2015-05-16 23:10:20 +02:00
|
|
|
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.
|
|
|
|
"""
|
2014-10-13 20:11:13 +02:00
|
|
|
super().__init__(parent)
|
2014-11-30 22:30:26 +01:00
|
|
|
self.ignored = False
|
2015-05-16 23:10:20 +02:00
|
|
|
self._socketname = _get_socketname(args)
|
2014-10-13 20:36:23 +02:00
|
|
|
self._remove_server()
|
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)
|
2014-10-13 20:11:13 +02:00
|
|
|
self._server = QLocalServer(self)
|
2015-05-16 23:10:20 +02:00
|
|
|
ok = self._server.listen(self._socketname)
|
2014-10-13 20:11:13 +02:00
|
|
|
if not ok:
|
2015-04-17 19:22:56 +02:00
|
|
|
if self._server.serverError() == QAbstractSocket.AddressInUseError:
|
|
|
|
raise AddressInUseError(self._server)
|
|
|
|
else:
|
|
|
|
raise ListenError(self._server)
|
2014-10-13 21:12:15 +02:00
|
|
|
self._server.newConnection.connect(self.handle_connection)
|
2014-10-13 20:11:13 +02:00
|
|
|
self._socket = None
|
|
|
|
|
2014-10-13 20:36:23 +02:00
|
|
|
def _remove_server(self):
|
|
|
|
"""Remove an existing server."""
|
2015-05-16 23:10:20 +02:00
|
|
|
ok = QLocalServer.removeServer(self._socketname)
|
2014-10-13 20:36:23 +02:00
|
|
|
if not ok:
|
2015-05-16 23:10:20 +02:00
|
|
|
raise Error("Error while removing server {}!".format(
|
|
|
|
self._socketname))
|
2014-10-13 20:36:23 +02:00
|
|
|
|
2014-10-13 20:11:13 +02:00
|
|
|
@pyqtSlot(int)
|
|
|
|
def on_error(self, error):
|
|
|
|
"""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()))
|
2014-10-13 20:11:13 +02:00
|
|
|
if error != 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."""
|
2014-11-30 22:30:26 +01:00
|
|
|
if self.ignored:
|
|
|
|
return
|
2014-10-13 20:11:13 +02:00
|
|
|
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()
|
2014-10-13 22:35:10 +02:00
|
|
|
self._socket = socket
|
2014-10-13 21:12:15 +02:00
|
|
|
socket.readyRead.connect(self.on_ready_read)
|
2014-10-13 22:35:10 +02:00
|
|
|
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):
|
2014-10-13 22:35:10 +02:00
|
|
|
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:11:13 +02:00
|
|
|
|
2014-10-13 20:37:09 +02:00
|
|
|
@pyqtSlot()
|
2014-10-13 20:11:13 +02:00
|
|
|
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()
|
2014-10-13 20:11:13 +02:00
|
|
|
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:11:13 +02:00
|
|
|
|
2014-10-13 20:37:09 +02:00
|
|
|
@pyqtSlot()
|
2014-10-13 20:11:13 +02:00
|
|
|
def on_ready_read(self):
|
|
|
|
"""Read json data from the client."""
|
2014-10-13 22:35:10 +02:00
|
|
|
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()
|
2014-11-02 21:53:12 +01:00
|
|
|
while self._socket is not None and self._socket.canReadLine():
|
2014-10-13 20:11:13 +02:00
|
|
|
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.")
|
2014-12-26 15:07:18 +01:00
|
|
|
log.ipc.debug("no args: {}".format(decoded.strip()))
|
2014-11-30 19:22:35 +01:00
|
|
|
return
|
2014-12-26 15:07:18 +01:00
|
|
|
cwd = json_data.get('cwd', None)
|
2015-05-01 14:44:41 +02:00
|
|
|
self.got_args.emit(args, cwd)
|
2014-10-13 20:11:13 +02:00
|
|
|
|
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:11:13 +02:00
|
|
|
|
2014-10-13 20:38:40 +02:00
|
|
|
|
2014-10-13 07:47:05 +02:00
|
|
|
def _socket_error(action, socket):
|
2015-04-17 19:22:56 +02:00
|
|
|
"""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
|
|
|
"""
|
2015-04-17 19:22:56 +02:00
|
|
|
raise Error("Error while {}: {} (error {})".format(
|
2014-10-13 07:47:05 +02:00
|
|
|
action, socket.errorString(), socket.error()))
|
|
|
|
|
|
|
|
|
2015-05-16 23:10:20 +02:00
|
|
|
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:
|
2015-05-16 23:10:20 +02:00
|
|
|
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()
|
2015-05-16 23:10:20 +02:00
|
|
|
socket.connectToServer(_get_socketname(args))
|
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")
|
2015-05-16 23:10:20 +02:00
|
|
|
json_data = {'args': args.command}
|
2014-12-26 15:07:18 +01:00
|
|
|
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)
|
2014-10-13 07:34:15 +02:00
|
|
|
socket.waitForBytesWritten(WRITE_TIMEOUT)
|
2014-10-13 07:49:26 +02:00
|
|
|
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:
|
2014-10-13 07:49:01 +02:00
|
|
|
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
|
2015-04-17 19:22:56 +02:00
|
|
|
|
|
|
|
|
|
|
|
def display_error(exc):
|
|
|
|
"""Display a message box with an IPC error."""
|
|
|
|
text = '{}\n\nMaybe another instance is running but frozen?'.format(exc)
|
|
|
|
msgbox = QMessageBox(QMessageBox.Critical, "Error while connecting to "
|
|
|
|
"running instance!", text)
|
|
|
|
msgbox.exec_()
|