Пример #1
0
class IPCServer(QObject):

    """IPC server to which clients connect to.

    Attributes:
        ignored: Whether requests are ignored (in exception hook).
        _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.
        _socketopts_ok: Set if using setSocketOptions is working with this
                        OS/Qt version.
        _atime_timer: Timer to update the atime of the socket regularly.

    Signals:
        got_args: Emitted when there was an IPC connection and arguments were
                  passed.
        got_args: Emitted with the raw data an IPC connection got.
        got_invalid_data: Emitted when there was invalid incoming data.
    """

    got_args = pyqtSignal(list, str, str)
    got_raw = pyqtSignal(bytes)
    got_invalid_data = pyqtSignal()

    def __init__(self, socketname, parent=None):
        """Start the IPC server and listen to commands.

        Args:
            socketname: The socketname to use.
            parent: The parent to be used.
        """
        super().__init__(parent)
        self.ignored = False
        self._socketname = socketname

        self._timer = usertypes.Timer(self, "ipc-timeout")
        self._timer.setInterval(READ_TIMEOUT)
        self._timer.timeout.connect(self.on_timeout)

        if os.name == "nt":  # pragma: no coverage
            self._atime_timer = None
        else:
            self._atime_timer = usertypes.Timer(self, "ipc-atime")
            self._atime_timer.setInterval(ATIME_INTERVAL)
            self._atime_timer.timeout.connect(self.update_atime)
            self._atime_timer.setTimerType(Qt.VeryCoarseTimer)

        self._server = QLocalServer(self)
        self._server.newConnection.connect(self.handle_connection)

        self._socket = None
        self._socketopts_ok = os.name == "nt"
        if self._socketopts_ok:  # pragma: no cover
            # If we use setSocketOptions on Unix with Qt < 5.4, we get a
            # NameError while listening...
            log.ipc.debug("Calling setSocketOptions")
            self._server.setSocketOptions(QLocalServer.UserAccessOption)
        else:  # pragma: no cover
            log.ipc.debug("Not calling setSocketOptions")

    def _remove_server(self):
        """Remove an existing server."""
        ok = QLocalServer.removeServer(self._socketname)
        if not ok:
            raise Error("Error while removing server {}!".format(self._socketname))

    def listen(self):
        """Start listening on self._socketname."""
        log.ipc.debug("Listening as {}".format(self._socketname))
        if self._atime_timer is not None:  # pragma: no branch
            self._atime_timer.start()
        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)
        if not self._socketopts_ok:  # pragma: no cover
            # If we use setSocketOptions on Unix with Qt < 5.4, we get a
            # NameError while listening.
            # (see b135569d5c6e68c735ea83f42e4baf51f7972281)
            #
            # Also, we don't get an AddressInUseError with Qt 5.5:
            # https://bugreports.qt.io/browse/QTBUG-48635
            #
            # This means we only use setSocketOption on Windows...
            os.chmod(self._server.fullServerName(), 0o700)

    @pyqtSlot(int)
    def on_error(self, err):
        """Raise SocketError on fatal errors."""
        if self._socket is None:
            # Sometimes this gets called from stale sockets.
            log.ipc.debug("In on_error with None socket!")
            return
        self._timer.stop()
        log.ipc.debug("Socket error {}: {}".format(self._socket.error(), self._socket.errorString()))
        if err != QLocalSocket.PeerClosedError:
            raise SocketError("handling IPC connection", self._socket)

    @pyqtSlot()
    def handle_connection(self):
        """Handle a new connection to the server."""
        if self.ignored:
            return
        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()
        if self._socket is None:
            log.ipc.debug("In on_disconnected with None socket!")
        else:
            self._socket.deleteLater()
            self._socket = None
        # Maybe another connection is waiting.
        self.handle_connection()

    def _handle_invalid_data(self):
        """Handle invalid data we got from a QLocalSocket."""
        log.ipc.error("Ignoring invalid IPC data.")
        self.got_invalid_data.emit()
        self._socket.error.connect(self.on_error)
        self._socket.disconnectFromServer()

    @pyqtSlot()
    def on_ready_read(self):
        """Read json data from the client."""
        if self._socket is None:
            # This happens when doing a connection while another one is already
            # active for some reason.
            log.ipc.warning("In on_ready_read with None socket!")
            return
        self._timer.start()
        while self._socket is not None and self._socket.canReadLine():
            data = bytes(self._socket.readLine())
            self.got_raw.emit(data)
            log.ipc.debug("Read from socket: {}".format(data))

            try:
                decoded = data.decode("utf-8")
            except UnicodeDecodeError:
                log.ipc.error("invalid utf-8: {}".format(binascii.hexlify(data)))
                self._handle_invalid_data()
                return

            log.ipc.debug("Processing: {}".format(decoded))
            try:
                json_data = json.loads(decoded)
            except ValueError:
                log.ipc.error("invalid json: {}".format(decoded.strip()))
                self._handle_invalid_data()
                return

            for name in ("args", "target_arg"):
                if name not in json_data:
                    log.ipc.error("Missing {}: {}".format(name, decoded.strip()))
                    self._handle_invalid_data()
                    return

            try:
                protocol_version = int(json_data["protocol_version"])
            except (KeyError, ValueError):
                log.ipc.error("invalid version: {}".format(decoded.strip()))
                self._handle_invalid_data()
                return

            if protocol_version != PROTOCOL_VERSION:
                log.ipc.error("incompatible version: expected {}, " "got {}".format(PROTOCOL_VERSION, protocol_version))
                self._handle_invalid_data()
                return

            cwd = json_data.get("cwd", None)
            self.got_args.emit(json_data["args"], json_data["target_arg"], cwd)

    @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.disconnectFromServer()
        if self._socket is not None:  # pragma: no cover
            # on_socket_disconnected sets it to None
            self._socket.waitForDisconnected(CONNECT_TIMEOUT)
        if self._socket is not None:  # pragma: no cover
            # on_socket_disconnected sets it to None
            self._socket.abort()

    @pyqtSlot()
    def update_atime(self):
        """Update the atime of the socket file all few hours.

        From the XDG basedir spec:

        To ensure that your files are not removed, they should have their
        access time timestamp modified at least once every 6 hours of monotonic
        time or the 'sticky' bit should be set on the file.
        """
        path = self._server.fullServerName()
        if not path:
            log.ipc.error("In update_atime with no server path!")
            return
        log.ipc.debug("Touching {}".format(path))
        os.utime(path)

    def shutdown(self):
        """Shut down the IPC server cleanly."""
        log.ipc.debug("Shutting down IPC")
        if self._socket is not None:
            self._socket.deleteLater()
            self._socket = None
        self._timer.stop()
        if self._atime_timer is not None:  # pragma: no branch
            self._atime_timer.stop()
            try:
                self._atime_timer.timeout.disconnect(self.update_atime)
            except TypeError:
                pass
        self._server.close()
        self._server.deleteLater()
        self._remove_server()
Пример #2
0
class IPCServer(QObject):
    """IPC server to which clients connect to.

    Attributes:
        ignored: Whether requests are ignored (in exception hook).
        _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.
        _atime_timer: Timer to update the atime of the socket regularly.

    Signals:
        got_args: Emitted when there was an IPC connection and arguments were
                  passed.
        got_args: Emitted with the raw data an IPC connection got.
        got_invalid_data: Emitted when there was invalid incoming data.
    """

    got_args = pyqtSignal(list, str, str)
    got_raw = pyqtSignal(bytes)
    got_invalid_data = pyqtSignal()

    def __init__(self, socketname, parent=None):
        """Start the IPC server and listen to commands.

        Args:
            socketname: The socketname to use.
            parent: The parent to be used.
        """
        super().__init__(parent)
        self.ignored = False
        self._socketname = socketname

        self._timer = usertypes.Timer(self, 'ipc-timeout')
        self._timer.setInterval(READ_TIMEOUT)
        self._timer.timeout.connect(self.on_timeout)

        if utils.is_windows:  # pragma: no cover
            self._atime_timer = None
        else:
            self._atime_timer = usertypes.Timer(self, 'ipc-atime')
            self._atime_timer.setInterval(ATIME_INTERVAL)
            self._atime_timer.timeout.connect(self.update_atime)
            self._atime_timer.setTimerType(Qt.VeryCoarseTimer)

        self._server = QLocalServer(self)
        self._server.newConnection.connect(  # type: ignore[attr-defined]
            self.handle_connection)

        self._socket = None
        self._old_socket = None

        if utils.is_windows:  # pragma: no cover
            # As a WORKAROUND for a Qt bug, we can't use UserAccessOption on Unix. If we
            # do, we don't get an AddressInUseError anymore:
            # https://bugreports.qt.io/browse/QTBUG-48635
            #
            # Thus, we only do so on Windows, and handle permissions manually in
            # listen() on Linux.
            log.ipc.debug("Calling setSocketOptions")
            self._server.setSocketOptions(QLocalServer.UserAccessOption)
        else:  # pragma: no cover
            log.ipc.debug("Not calling setSocketOptions")

    def _remove_server(self):
        """Remove an existing server."""
        ok = QLocalServer.removeServer(self._socketname)
        if not ok:
            raise Error("Error while removing server {}!".format(
                self._socketname))

    def listen(self):
        """Start listening on self._socketname."""
        log.ipc.debug("Listening as {}".format(self._socketname))
        if self._atime_timer is not None:  # pragma: no branch
            self._atime_timer.start()
        self._remove_server()
        ok = self._server.listen(self._socketname)
        if not ok:
            if self._server.serverError() == QAbstractSocket.AddressInUseError:
                raise AddressInUseError(self._server)
            raise ListenError(self._server)

        if not utils.is_windows:  # pragma: no cover
            # WORKAROUND for QTBUG-48635, see the comment in __init__ for details.
            try:
                os.chmod(self._server.fullServerName(), 0o700)
            except FileNotFoundError:
                # https://github.com/qutebrowser/qutebrowser/issues/1530
                # The server doesn't actually exist even if ok was reported as
                # True, so report this as an error.
                raise ListenError(self._server)

    @pyqtSlot('QLocalSocket::LocalSocketError')
    def on_error(self, err):
        """Raise SocketError on fatal errors."""
        if self._socket is None:
            # Sometimes this gets called from stale sockets.
            log.ipc.debug("In on_error with None socket!")
            return
        self._timer.stop()
        log.ipc.debug("Socket 0x{:x}: error {}: {}".format(
            id(self._socket), self._socket.error(),
            self._socket.errorString()))
        if err != QLocalSocket.PeerClosedError:
            raise SocketError("handling IPC connection", self._socket)

    @pyqtSlot()
    def handle_connection(self):
        """Handle a new connection to the server."""
        if self.ignored:
            return
        if self._socket is not None:
            log.ipc.debug("Got new connection but ignoring it because we're "
                          "still handling another one (0x{:x}).".format(
                              id(self._socket)))
            return
        socket = self._server.nextPendingConnection()
        if socket is None:
            log.ipc.debug(  # type: ignore[unreachable]
                "No new connection to handle.")
            return
        log.ipc.debug("Client connected (socket 0x{:x}).".format(id(socket)))
        self._socket = socket
        self._timer.start()
        socket.readyRead.connect(  # type: ignore[attr-defined]
            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)  # type: ignore[attr-defined]
        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(  # type: ignore[attr-defined]
            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 from socket 0x{:x}.".format(
            id(self._socket)))
        self._timer.stop()
        if self._old_socket is not None:
            self._old_socket.deleteLater()
        self._old_socket = self._socket
        self._socket = None
        # Maybe another connection is waiting.
        self.handle_connection()

    def _handle_invalid_data(self):
        """Handle invalid data we got from a QLocalSocket."""
        assert self._socket is not None
        log.ipc.error("Ignoring invalid IPC data from socket 0x{:x}.".format(
            id(self._socket)))
        self.got_invalid_data.emit()
        self._socket.error.connect(self.on_error)
        self._socket.disconnectFromServer()

    def _handle_data(self, data):
        """Handle data (as bytes) we got from on_ready_read."""
        try:
            decoded = data.decode('utf-8')
        except UnicodeDecodeError:
            log.ipc.error("invalid utf-8: {!r}".format(binascii.hexlify(data)))
            self._handle_invalid_data()
            return

        log.ipc.debug("Processing: {}".format(decoded))
        try:
            json_data = json.loads(decoded)
        except ValueError:
            log.ipc.error("invalid json: {}".format(decoded.strip()))
            self._handle_invalid_data()
            return

        for name in ['args', 'target_arg']:
            if name not in json_data:
                log.ipc.error("Missing {}: {}".format(name, decoded.strip()))
                self._handle_invalid_data()
                return

        try:
            protocol_version = int(json_data['protocol_version'])
        except (KeyError, ValueError):
            log.ipc.error("invalid version: {}".format(decoded.strip()))
            self._handle_invalid_data()
            return

        if protocol_version != PROTOCOL_VERSION:
            log.ipc.error("incompatible version: expected {}, got {}".format(
                PROTOCOL_VERSION, protocol_version))
            self._handle_invalid_data()
            return

        args = json_data['args']

        target_arg = json_data['target_arg']
        if target_arg is None:
            # https://www.riverbankcomputing.com/pipermail/pyqt/2016-April/037375.html
            target_arg = ''

        cwd = json_data.get('cwd', '')
        assert cwd is not None

        self.got_args.emit(args, target_arg, cwd)

    @pyqtSlot()
    def on_ready_read(self):
        """Read json data from the client."""
        if self._socket is None:  # pragma: no cover
            # This happens when doing a connection while another one is already
            # active for some reason.
            if self._old_socket is None:
                log.ipc.warning("In on_ready_read with None socket and "
                                "old_socket!")
                return
            log.ipc.debug("In on_ready_read with None socket!")
            socket = self._old_socket
        else:
            socket = self._socket

        if sip.isdeleted(socket):  # pragma: no cover
            log.ipc.warning("Ignoring deleted IPC socket")
            return

        self._timer.stop()
        while socket is not None and socket.canReadLine():
            data = bytes(socket.readLine())
            self.got_raw.emit(data)
            log.ipc.debug("Read from socket 0x{:x}: {!r}".format(
                id(socket), data))
            self._handle_data(data)

        if self._socket is not None:
            self._timer.start()

    @pyqtSlot()
    def on_timeout(self):
        """Cancel the current connection if it was idle for too long."""
        assert self._socket is not None
        log.ipc.error("IPC connection timed out "
                      "(socket 0x{:x}).".format(id(self._socket)))
        self._socket.disconnectFromServer()
        if self._socket is not None:  # pragma: no cover
            # on_socket_disconnected sets it to None
            self._socket.waitForDisconnected(CONNECT_TIMEOUT)
        if self._socket is not None:  # pragma: no cover
            # on_socket_disconnected sets it to None
            self._socket.abort()

    @pyqtSlot()
    def update_atime(self):
        """Update the atime of the socket file all few hours.

        From the XDG basedir spec:

        To ensure that your files are not removed, they should have their
        access time timestamp modified at least once every 6 hours of monotonic
        time or the 'sticky' bit should be set on the file.
        """
        path = self._server.fullServerName()
        if not path:
            log.ipc.error("In update_atime with no server path!")
            return

        log.ipc.debug("Touching {}".format(path))

        try:
            os.utime(path)
        except OSError:
            log.ipc.exception("Failed to update IPC socket, trying to "
                              "re-listen...")
            self._server.close()
            self.listen()

    @pyqtSlot()
    def shutdown(self):
        """Shut down the IPC server cleanly."""
        log.ipc.debug("Shutting down IPC (socket 0x{:x})".format(
            id(self._socket)))
        if self._socket is not None:
            self._socket.deleteLater()
            self._socket = None
        self._timer.stop()
        if self._atime_timer is not None:  # pragma: no branch
            self._atime_timer.stop()
            try:
                self._atime_timer.timeout.disconnect(self.update_atime)
            except TypeError:
                pass
        self._server.close()
        self._server.deleteLater()
        self._remove_server()
Пример #3
0
def qlocalserver(qapp):
    server = QLocalServer()
    yield server
    server.close()
    server.deleteLater()
Пример #4
0
def qlocalserver(qapp):
    server = QLocalServer()
    yield server
    server.close()
    server.deleteLater()
Пример #5
0
class IPCServer(QObject):
    """IPC server to which clients connect to.

    Attributes:
        ignored: Whether requests are ignored (in exception hook).
        _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.ignored = False
        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.ignored:
            return
        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 is not None and 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
            log.ipc.debug("Processing: {}".format(decoded))
            try:
                json_data = json.loads(decoded)
            except ValueError:
                log.ipc.error("Ignoring invalid IPC data.")
                log.ipc.debug("invalid json: {}".format(decoded.strip()))
                return
            try:
                args = json_data['args']
            except KeyError:
                log.ipc.error("Ignoring invalid IPC data.")
                log.ipc.debug("no args: {}".format(decoded.strip()))
                return
            cwd = json_data.get('cwd', None)
            app = objreg.get('app')
            app.process_args(args, via_ipc=True, cwd=cwd)

    @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()
Пример #6
0
class IPCServer(QObject):

    """IPC server to which clients connect to.

    Attributes:
        ignored: Whether requests are ignored (in exception hook).
        _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.ignored = False
        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.ignored:
            return
        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 is not None and 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
            log.ipc.debug("Processing: {}".format(decoded))
            try:
                json_data = json.loads(decoded)
            except ValueError:
                log.ipc.error("Ignoring invalid IPC data.")
                log.ipc.debug("invalid json: {}".format(decoded.strip()))
                return
            try:
                args = json_data['args']
            except KeyError:
                log.ipc.error("Ignoring invalid IPC data.")
                log.ipc.debug("no args: {}".format(decoded.strip()))
                return
            cwd = json_data.get('cwd', None)
            app = objreg.get('app')
            app.process_pos_args(args, via_ipc=True, cwd=cwd)

    @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()
Пример #7
0
class IPCServer(QObject):
    """IPC server to which clients connect to.

    Attributes:
        ignored: Whether requests are ignored (in exception hook).
        _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.
        _socketopts_ok: Set if using setSocketOptions is working with this
                        OS/Qt version.
        _atime_timer: Timer to update the atime of the socket regularly.

    Signals:
        got_args: Emitted when there was an IPC connection and arguments were
                  passed.
        got_args: Emitted with the raw data an IPC connection got.
        got_invalid_data: Emitted when there was invalid incoming data.
    """

    got_args = pyqtSignal(list, str, str)
    got_raw = pyqtSignal(bytes)
    got_invalid_data = pyqtSignal()

    def __init__(self, socketname, parent=None):
        """Start the IPC server and listen to commands.

        Args:
            socketname: The socketname to use.
            parent: The parent to be used.
        """
        super().__init__(parent)
        self.ignored = False
        self._socketname = socketname

        self._timer = usertypes.Timer(self, 'ipc-timeout')
        self._timer.setInterval(READ_TIMEOUT)
        self._timer.timeout.connect(self.on_timeout)

        if os.name == 'nt':  # pragma: no coverage
            self._atime_timer = None
        else:
            self._atime_timer = usertypes.Timer(self, 'ipc-atime')
            self._atime_timer.setInterval(ATIME_INTERVAL)
            self._atime_timer.timeout.connect(self.update_atime)
            self._atime_timer.setTimerType(Qt.VeryCoarseTimer)

        self._server = QLocalServer(self)
        self._server.newConnection.connect(self.handle_connection)

        self._socket = None
        self._socketopts_ok = os.name == 'nt'
        if self._socketopts_ok:  # pragma: no cover
            # If we use setSocketOptions on Unix with Qt < 5.4, we get a
            # NameError while listening...
            log.ipc.debug("Calling setSocketOptions")
            self._server.setSocketOptions(QLocalServer.UserAccessOption)
        else:  # pragma: no cover
            log.ipc.debug("Not calling setSocketOptions")

    def _remove_server(self):
        """Remove an existing server."""
        ok = QLocalServer.removeServer(self._socketname)
        if not ok:
            raise Error("Error while removing server {}!".format(
                self._socketname))

    def listen(self):
        """Start listening on self._socketname."""
        log.ipc.debug("Listening as {}".format(self._socketname))
        if self._atime_timer is not None:  # pragma: no branch
            self._atime_timer.start()
        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)
        if not self._socketopts_ok:  # pragma: no cover
            # If we use setSocketOptions on Unix with Qt < 5.4, we get a
            # NameError while listening.
            # (see b135569d5c6e68c735ea83f42e4baf51f7972281)
            #
            # Also, we don't get an AddressInUseError with Qt 5.5:
            # https://bugreports.qt.io/browse/QTBUG-48635
            #
            # This means we only use setSocketOption on Windows...
            os.chmod(self._server.fullServerName(), 0o700)

    @pyqtSlot(int)
    def on_error(self, err):
        """Raise SocketError on fatal errors."""
        if self._socket is None:
            # Sometimes this gets called from stale sockets.
            log.ipc.debug("In on_error with None socket!")
            return
        self._timer.stop()
        log.ipc.debug("Socket error {}: {}".format(self._socket.error(),
                                                   self._socket.errorString()))
        if err != QLocalSocket.PeerClosedError:
            raise SocketError("handling IPC connection", self._socket)

    @pyqtSlot()
    def handle_connection(self):
        """Handle a new connection to the server."""
        if self.ignored:
            return
        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()
        if self._socket is None:
            log.ipc.debug("In on_disconnected with None socket!")
        else:
            self._socket.deleteLater()
            self._socket = None
        # Maybe another connection is waiting.
        self.handle_connection()

    def _handle_invalid_data(self):
        """Handle invalid data we got from a QLocalSocket."""
        log.ipc.error("Ignoring invalid IPC data.")
        self.got_invalid_data.emit()
        self._socket.error.connect(self.on_error)
        self._socket.disconnectFromServer()

    @pyqtSlot()
    def on_ready_read(self):
        """Read json data from the client."""
        if self._socket is None:
            # This happens when doing a connection while another one is already
            # active for some reason.
            log.ipc.warning("In on_ready_read with None socket!")
            return
        self._timer.start()
        while self._socket is not None and self._socket.canReadLine():
            data = bytes(self._socket.readLine())
            self.got_raw.emit(data)
            log.ipc.debug("Read from socket: {}".format(data))

            try:
                decoded = data.decode('utf-8')
            except UnicodeDecodeError:
                log.ipc.error("invalid utf-8: {}".format(
                    binascii.hexlify(data)))
                self._handle_invalid_data()
                return

            log.ipc.debug("Processing: {}".format(decoded))
            try:
                json_data = json.loads(decoded)
            except ValueError:
                log.ipc.error("invalid json: {}".format(decoded.strip()))
                self._handle_invalid_data()
                return

            for name in ('args', 'target_arg'):
                if name not in json_data:
                    log.ipc.error("Missing {}: {}".format(
                        name, decoded.strip()))
                    self._handle_invalid_data()
                    return

            try:
                protocol_version = int(json_data['protocol_version'])
            except (KeyError, ValueError):
                log.ipc.error("invalid version: {}".format(decoded.strip()))
                self._handle_invalid_data()
                return

            if protocol_version != PROTOCOL_VERSION:
                log.ipc.error("incompatible version: expected {}, "
                              "got {}".format(PROTOCOL_VERSION,
                                              protocol_version))
                self._handle_invalid_data()
                return

            cwd = json_data.get('cwd', None)
            self.got_args.emit(json_data['args'], json_data['target_arg'], cwd)

    @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.disconnectFromServer()
        if self._socket is not None:  # pragma: no cover
            # on_socket_disconnected sets it to None
            self._socket.waitForDisconnected(CONNECT_TIMEOUT)
        if self._socket is not None:  # pragma: no cover
            # on_socket_disconnected sets it to None
            self._socket.abort()

    @pyqtSlot()
    def update_atime(self):
        """Update the atime of the socket file all few hours.

        From the XDG basedir spec:

        To ensure that your files are not removed, they should have their
        access time timestamp modified at least once every 6 hours of monotonic
        time or the 'sticky' bit should be set on the file.
        """
        path = self._server.fullServerName()
        if not path:
            log.ipc.error("In update_atime with no server path!")
            return
        log.ipc.debug("Touching {}".format(path))
        os.utime(path)

    def shutdown(self):
        """Shut down the IPC server cleanly."""
        log.ipc.debug("Shutting down IPC")
        if self._socket is not None:
            self._socket.deleteLater()
            self._socket = None
        self._timer.stop()
        if self._atime_timer is not None:  # pragma: no branch
            self._atime_timer.stop()
            try:
                self._atime_timer.timeout.disconnect(self.update_atime)
            except TypeError:
                pass
        self._server.close()
        self._server.deleteLater()
        self._remove_server()