1# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
2
3# Copyright 2014-2021 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
4#
5# This file is part of qutebrowser.
6#
7# qutebrowser is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# qutebrowser is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with qutebrowser.  If not, see <https://www.gnu.org/licenses/>.
19
20"""Utilities for IPC with existing instances."""
21
22import os
23import time
24import json
25import getpass
26import binascii
27import hashlib
28
29from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, Qt
30from PyQt5.QtNetwork import QLocalSocket, QLocalServer, QAbstractSocket
31
32import qutebrowser
33from qutebrowser.utils import log, usertypes, error, standarddir, utils
34from qutebrowser.qt import sip
35
36
37CONNECT_TIMEOUT = 100  # timeout for connecting/disconnecting
38WRITE_TIMEOUT = 1000
39READ_TIMEOUT = 5000
40ATIME_INTERVAL = 5000 * 60  # 5 minutes
41PROTOCOL_VERSION = 1
42
43
44# The ipc server instance
45server = None
46
47
48def _get_socketname_windows(basedir):
49    """Get a socketname to use for Windows."""
50    try:
51        username = getpass.getuser()
52    except ImportError:
53        # getpass.getuser() first tries a couple of environment variables. If
54        # none of those are set (i.e., USERNAME is missing), it tries to import
55        # the "pwd" module which is unavailable on Windows.
56        raise Error("Could not find username. This should only happen if "
57                    "there is a bug in the application launching qutebrowser, "
58                    "preventing the USERNAME environment variable from being "
59                    "passed. If you know more about when this happens, please "
60                    "report this to mail@qutebrowser.org.")
61
62    parts = ['qutebrowser', username]
63    if basedir is not None:
64        md5 = hashlib.md5(basedir.encode('utf-8')).hexdigest()
65        parts.append(md5)
66    return '-'.join(parts)
67
68
69def _get_socketname(basedir):
70    """Get a socketname to use."""
71    if utils.is_windows:  # pragma: no cover
72        return _get_socketname_windows(basedir)
73
74    parts_to_hash = [getpass.getuser()]
75    if basedir is not None:
76        parts_to_hash.append(basedir)
77
78    data_to_hash = '-'.join(parts_to_hash).encode('utf-8')
79    md5 = hashlib.md5(data_to_hash).hexdigest()
80
81    prefix = 'i-' if utils.is_mac else 'ipc-'
82    filename = '{}{}'.format(prefix, md5)
83    return os.path.join(standarddir.runtime(), filename)
84
85
86class Error(Exception):
87
88    """Base class for IPC exceptions."""
89
90
91class SocketError(Error):
92
93    """Exception raised when there was an error with a QLocalSocket.
94
95    Args:
96        code: The error code.
97        message: The error message.
98        action: The action which was taken when the error happened.
99    """
100
101    def __init__(self, action, socket):
102        """Constructor.
103
104        Args:
105            action: The action which was taken when the error happened.
106            socket: The QLocalSocket which has the error set.
107        """
108        super().__init__()
109        self.action = action
110        self.code = socket.error()
111        self.message = socket.errorString()
112
113    def __str__(self):
114        return "Error while {}: {} (error {})".format(
115            self.action, self.message, self.code)
116
117
118class ListenError(Error):
119
120    """Exception raised when there was a problem with listening to IPC.
121
122    Args:
123        code: The error code.
124        message: The error message.
125    """
126
127    def __init__(self, local_server):
128        """Constructor.
129
130        Args:
131            local_server: The QLocalServer which has the error set.
132        """
133        super().__init__()
134        self.code = local_server.serverError()
135        self.message = local_server.errorString()
136
137    def __str__(self):
138        return "Error while listening to IPC server: {} (error {})".format(
139            self.message, self.code)
140
141
142class AddressInUseError(ListenError):
143
144    """Emitted when the server address is already in use."""
145
146
147class IPCServer(QObject):
148
149    """IPC server to which clients connect to.
150
151    Attributes:
152        ignored: Whether requests are ignored (in exception hook).
153        _timer: A timer to handle timeouts.
154        _server: A QLocalServer to accept new connections.
155        _socket: The QLocalSocket we're currently connected to.
156        _socketname: The socketname to use.
157        _atime_timer: Timer to update the atime of the socket regularly.
158
159    Signals:
160        got_args: Emitted when there was an IPC connection and arguments were
161                  passed.
162        got_args: Emitted with the raw data an IPC connection got.
163        got_invalid_data: Emitted when there was invalid incoming data.
164    """
165
166    got_args = pyqtSignal(list, str, str)
167    got_raw = pyqtSignal(bytes)
168    got_invalid_data = pyqtSignal()
169
170    def __init__(self, socketname, parent=None):
171        """Start the IPC server and listen to commands.
172
173        Args:
174            socketname: The socketname to use.
175            parent: The parent to be used.
176        """
177        super().__init__(parent)
178        self.ignored = False
179        self._socketname = socketname
180
181        self._timer = usertypes.Timer(self, 'ipc-timeout')
182        self._timer.setInterval(READ_TIMEOUT)
183        self._timer.timeout.connect(self.on_timeout)
184
185        if utils.is_windows:  # pragma: no cover
186            self._atime_timer = None
187        else:
188            self._atime_timer = usertypes.Timer(self, 'ipc-atime')
189            self._atime_timer.setInterval(ATIME_INTERVAL)
190            self._atime_timer.timeout.connect(self.update_atime)
191            self._atime_timer.setTimerType(Qt.VeryCoarseTimer)
192
193        self._server = QLocalServer(self)
194        self._server.newConnection.connect(  # type: ignore[attr-defined]
195            self.handle_connection)
196
197        self._socket = None
198        self._old_socket = None
199
200        if utils.is_windows:  # pragma: no cover
201            # As a WORKAROUND for a Qt bug, we can't use UserAccessOption on Unix. If we
202            # do, we don't get an AddressInUseError anymore:
203            # https://bugreports.qt.io/browse/QTBUG-48635
204            #
205            # Thus, we only do so on Windows, and handle permissions manually in
206            # listen() on Linux.
207            log.ipc.debug("Calling setSocketOptions")
208            self._server.setSocketOptions(QLocalServer.UserAccessOption)
209        else:  # pragma: no cover
210            log.ipc.debug("Not calling setSocketOptions")
211
212    def _remove_server(self):
213        """Remove an existing server."""
214        ok = QLocalServer.removeServer(self._socketname)
215        if not ok:
216            raise Error("Error while removing server {}!".format(
217                self._socketname))
218
219    def listen(self):
220        """Start listening on self._socketname."""
221        log.ipc.debug("Listening as {}".format(self._socketname))
222        if self._atime_timer is not None:  # pragma: no branch
223            self._atime_timer.start()
224        self._remove_server()
225        ok = self._server.listen(self._socketname)
226        if not ok:
227            if self._server.serverError() == QAbstractSocket.AddressInUseError:
228                raise AddressInUseError(self._server)
229            raise ListenError(self._server)
230
231        if not utils.is_windows:  # pragma: no cover
232            # WORKAROUND for QTBUG-48635, see the comment in __init__ for details.
233            try:
234                os.chmod(self._server.fullServerName(), 0o700)
235            except FileNotFoundError:
236                # https://github.com/qutebrowser/qutebrowser/issues/1530
237                # The server doesn't actually exist even if ok was reported as
238                # True, so report this as an error.
239                raise ListenError(self._server)
240
241    @pyqtSlot('QLocalSocket::LocalSocketError')
242    def on_error(self, err):
243        """Raise SocketError on fatal errors."""
244        if self._socket is None:
245            # Sometimes this gets called from stale sockets.
246            log.ipc.debug("In on_error with None socket!")
247            return
248        self._timer.stop()
249        log.ipc.debug("Socket 0x{:x}: error {}: {}".format(
250            id(self._socket), self._socket.error(),
251            self._socket.errorString()))
252        if err != QLocalSocket.PeerClosedError:
253            raise SocketError("handling IPC connection", self._socket)
254
255    @pyqtSlot()
256    def handle_connection(self):
257        """Handle a new connection to the server."""
258        if self.ignored:
259            return
260        if self._socket is not None:
261            log.ipc.debug("Got new connection but ignoring it because we're "
262                          "still handling another one (0x{:x}).".format(
263                              id(self._socket)))
264            return
265        socket = self._server.nextPendingConnection()
266        if socket is None:
267            log.ipc.debug(  # type: ignore[unreachable]
268                "No new connection to handle.")
269            return
270        log.ipc.debug("Client connected (socket 0x{:x}).".format(id(socket)))
271        self._socket = socket
272        self._timer.start()
273        socket.readyRead.connect(  # type: ignore[attr-defined]
274            self.on_ready_read)
275        if socket.canReadLine():
276            log.ipc.debug("We can read a line immediately.")
277            self.on_ready_read()
278        socket.error.connect(self.on_error)  # type: ignore[attr-defined]
279        if socket.error() not in [QLocalSocket.UnknownSocketError,
280                                  QLocalSocket.PeerClosedError]:
281            log.ipc.debug("We got an error immediately.")
282            self.on_error(socket.error())
283        socket.disconnected.connect(  # type: ignore[attr-defined]
284            self.on_disconnected)
285        if socket.state() == QLocalSocket.UnconnectedState:
286            log.ipc.debug("Socket was disconnected immediately.")
287            self.on_disconnected()
288
289    @pyqtSlot()
290    def on_disconnected(self):
291        """Clean up socket when the client disconnected."""
292        log.ipc.debug("Client disconnected from socket 0x{:x}.".format(
293            id(self._socket)))
294        self._timer.stop()
295        if self._old_socket is not None:
296            self._old_socket.deleteLater()
297        self._old_socket = self._socket
298        self._socket = None
299        # Maybe another connection is waiting.
300        self.handle_connection()
301
302    def _handle_invalid_data(self):
303        """Handle invalid data we got from a QLocalSocket."""
304        assert self._socket is not None
305        log.ipc.error("Ignoring invalid IPC data from socket 0x{:x}.".format(
306            id(self._socket)))
307        self.got_invalid_data.emit()
308        self._socket.error.connect(self.on_error)
309        self._socket.disconnectFromServer()
310
311    def _handle_data(self, data):
312        """Handle data (as bytes) we got from on_ready_read."""
313        try:
314            decoded = data.decode('utf-8')
315        except UnicodeDecodeError:
316            log.ipc.error("invalid utf-8: {!r}".format(binascii.hexlify(data)))
317            self._handle_invalid_data()
318            return
319
320        log.ipc.debug("Processing: {}".format(decoded))
321        try:
322            json_data = json.loads(decoded)
323        except ValueError:
324            log.ipc.error("invalid json: {}".format(decoded.strip()))
325            self._handle_invalid_data()
326            return
327
328        for name in ['args', 'target_arg']:
329            if name not in json_data:
330                log.ipc.error("Missing {}: {}".format(name, decoded.strip()))
331                self._handle_invalid_data()
332                return
333
334        try:
335            protocol_version = int(json_data['protocol_version'])
336        except (KeyError, ValueError):
337            log.ipc.error("invalid version: {}".format(decoded.strip()))
338            self._handle_invalid_data()
339            return
340
341        if protocol_version != PROTOCOL_VERSION:
342            log.ipc.error("incompatible version: expected {}, got {}".format(
343                PROTOCOL_VERSION, protocol_version))
344            self._handle_invalid_data()
345            return
346
347        args = json_data['args']
348
349        target_arg = json_data['target_arg']
350        if target_arg is None:
351            # https://www.riverbankcomputing.com/pipermail/pyqt/2016-April/037375.html
352            target_arg = ''
353
354        cwd = json_data.get('cwd', '')
355        assert cwd is not None
356
357        self.got_args.emit(args, target_arg, cwd)
358
359    def _get_socket(self, warn=True):
360        """Get the current socket for on_ready_read.
361
362        Arguments:
363            warn: Whether to warn if no socket was found.
364        """
365        if self._socket is None:  # pragma: no cover
366            # This happens when doing a connection while another one is already
367            # active for some reason.
368            if self._old_socket is None:
369                if warn:
370                    log.ipc.warning("In _get_socket with None socket and old_socket!")
371                return None
372            log.ipc.debug("In _get_socket with None socket!")
373            socket = self._old_socket
374        else:
375            socket = self._socket
376
377        if sip.isdeleted(socket):  # pragma: no cover
378            log.ipc.warning("Ignoring deleted IPC socket")
379            return None
380
381        return socket
382
383    @pyqtSlot()
384    def on_ready_read(self):
385        """Read json data from the client."""
386        self._timer.stop()
387
388        socket = self._get_socket()
389        while socket is not None and socket.canReadLine():
390            data = bytes(socket.readLine())
391            self.got_raw.emit(data)
392            log.ipc.debug("Read from socket 0x{:x}: {!r}".format(
393                id(socket), data))
394            self._handle_data(data)
395            socket = self._get_socket(warn=False)
396
397        if self._socket is not None:
398            self._timer.start()
399
400    @pyqtSlot()
401    def on_timeout(self):
402        """Cancel the current connection if it was idle for too long."""
403        assert self._socket is not None
404        log.ipc.error("IPC connection timed out "
405                      "(socket 0x{:x}).".format(id(self._socket)))
406        self._socket.disconnectFromServer()
407        if self._socket is not None:  # pragma: no cover
408            # on_socket_disconnected sets it to None
409            self._socket.waitForDisconnected(CONNECT_TIMEOUT)
410        if self._socket is not None:  # pragma: no cover
411            # on_socket_disconnected sets it to None
412            self._socket.abort()
413
414    @pyqtSlot()
415    def update_atime(self):
416        """Update the atime of the socket file all few hours.
417
418        From the XDG basedir spec:
419
420        To ensure that your files are not removed, they should have their
421        access time timestamp modified at least once every 6 hours of monotonic
422        time or the 'sticky' bit should be set on the file.
423        """
424        path = self._server.fullServerName()
425        if not path:
426            log.ipc.error("In update_atime with no server path!")
427            return
428
429        log.ipc.debug("Touching {}".format(path))
430
431        try:
432            os.utime(path)
433        except OSError:
434            log.ipc.exception("Failed to update IPC socket, trying to "
435                              "re-listen...")
436            self._server.close()
437            self.listen()
438
439    @pyqtSlot()
440    def shutdown(self):
441        """Shut down the IPC server cleanly."""
442        log.ipc.debug("Shutting down IPC (socket 0x{:x})".format(
443            id(self._socket)))
444        if self._socket is not None:
445            self._socket.deleteLater()
446            self._socket = None
447        self._timer.stop()
448        if self._atime_timer is not None:  # pragma: no branch
449            self._atime_timer.stop()
450            try:
451                self._atime_timer.timeout.disconnect(self.update_atime)
452            except TypeError:
453                pass
454        self._server.close()
455        self._server.deleteLater()
456        self._remove_server()
457
458
459def send_to_running_instance(socketname, command, target_arg, *, socket=None):
460    """Try to send a commandline to a running instance.
461
462    Blocks for CONNECT_TIMEOUT ms.
463
464    Args:
465        socketname: The name which should be used for the socket.
466        command: The command to send to the running instance.
467        target_arg: --target command line argument
468        socket: The socket to read data from, or None.
469
470    Return:
471        True if connecting was successful, False if no connection was made.
472    """
473    if socket is None:
474        socket = QLocalSocket()
475
476    log.ipc.debug("Connecting to {}".format(socketname))
477    socket.connectToServer(socketname)
478
479    connected = socket.waitForConnected(CONNECT_TIMEOUT)
480    if connected:
481        log.ipc.info("Opening in existing instance")
482        json_data = {'args': command, 'target_arg': target_arg,
483                     'version': qutebrowser.__version__,
484                     'protocol_version': PROTOCOL_VERSION}
485        try:
486            cwd = os.getcwd()
487        except OSError:
488            pass
489        else:
490            json_data['cwd'] = cwd
491        line = json.dumps(json_data) + '\n'
492        data = line.encode('utf-8')
493        log.ipc.debug("Writing: {!r}".format(data))
494        socket.writeData(data)
495        socket.waitForBytesWritten(WRITE_TIMEOUT)
496        if socket.error() != QLocalSocket.UnknownSocketError:
497            raise SocketError("writing to running instance", socket)
498        socket.disconnectFromServer()
499        if socket.state() != QLocalSocket.UnconnectedState:
500            socket.waitForDisconnected(CONNECT_TIMEOUT)
501        return True
502    else:
503        if socket.error() not in [QLocalSocket.ConnectionRefusedError,
504                                  QLocalSocket.ServerNotFoundError]:
505            raise SocketError("connecting to running instance", socket)
506        log.ipc.debug("No existing instance present (error {})".format(
507            socket.error()))
508        return False
509
510
511def display_error(exc, args):
512    """Display a message box with an IPC error."""
513    error.handle_fatal_exc(
514        exc, "Error while connecting to running instance!",
515        no_err_windows=args.no_err_windows)
516
517
518def send_or_listen(args):
519    """Send the args to a running instance or start a new IPCServer.
520
521    Args:
522        args: The argparse namespace.
523
524    Return:
525        The IPCServer instance if no running instance was detected.
526        None if an instance was running and received our request.
527    """
528    global server
529    try:
530        socketname = _get_socketname(args.basedir)
531        try:
532            sent = send_to_running_instance(socketname, args.command,
533                                            args.target)
534            if sent:
535                return None
536            log.init.debug("Starting IPC server...")
537            server = IPCServer(socketname)
538            server.listen()
539            return server
540        except AddressInUseError:
541            # This could be a race condition...
542            log.init.debug("Got AddressInUseError, trying again.")
543            time.sleep(0.5)
544            sent = send_to_running_instance(socketname, args.command,
545                                            args.target)
546            if sent:
547                return None
548            else:
549                raise
550    except Error as e:
551        display_error(e, args)
552        raise
553