From 7f72565706b23882ec558f438d5220c57e4e58c4 Mon Sep 17 00:00:00 2001 From: Coiby Xu Date: Fri, 27 Nov 2020 20:53:51 +0800 Subject: [PATCH 1/4] single out the code of the IPC client --- qutebrowser/misc/ipc.py | 63 +------------ qutebrowser/misc/ipcclient.py | 166 ++++++++++++++++++++++++++++++++++ qutebrowser/qutebrowser.py | 12 +++ 3 files changed, 180 insertions(+), 61 deletions(-) create mode 100644 qutebrowser/misc/ipcclient.py diff --git a/qutebrowser/misc/ipc.py b/qutebrowser/misc/ipc.py index eefa2e3f3..0ab5d76b2 100644 --- a/qutebrowser/misc/ipc.py +++ b/qutebrowser/misc/ipc.py @@ -15,8 +15,8 @@ from typing import Optional from qutebrowser.qt.core import pyqtSignal, pyqtSlot, QObject, Qt from qutebrowser.qt.network import QLocalSocket, QLocalServer, QAbstractSocket -import qutebrowser -from qutebrowser.utils import log, usertypes, error, standarddir, utils, debug, qtutils +from qutebrowser.utils import log, usertypes, standarddir, utils, debug, qtutils +from qutebrowser.misc.ipcclient import send_to_running_instance, display_error from qutebrowser.qt import sip @@ -462,65 +462,6 @@ class IPCServer(QObject): self._server = None -def send_to_running_instance(socketname, command, target_arg, *, socket=None): - """Try to send a commandline to a running instance. - - Blocks for CONNECT_TIMEOUT ms. - - Args: - socketname: The name which should be used for the socket. - command: The command to send to the running instance. - target_arg: --target command line argument - socket: The socket to read data from, or None. - - Return: - True if connecting was successful, False if no connection was made. - """ - if socket is None: - socket = QLocalSocket() - - log.ipc.debug("Connecting to {}".format(socketname)) - socket.connectToServer(socketname) - - connected = socket.waitForConnected(CONNECT_TIMEOUT) - if connected: - log.ipc.info("Opening in existing instance") - json_data = {'args': command, 'target_arg': target_arg, - 'version': qutebrowser.__version__, - 'protocol_version': PROTOCOL_VERSION} - try: - cwd = os.getcwd() - except OSError: - pass - else: - json_data['cwd'] = cwd - line = json.dumps(json_data) + '\n' - data = line.encode('utf-8') - log.ipc.debug("Writing: {!r}".format(data)) - socket.writeData(data) - socket.waitForBytesWritten(WRITE_TIMEOUT) - if socket.error() != QLocalSocket.LocalSocketError.UnknownSocketError: - raise SocketError("writing to running instance", socket) - socket.disconnectFromServer() - if socket.state() != QLocalSocket.LocalSocketState.UnconnectedState: - socket.waitForDisconnected(CONNECT_TIMEOUT) - return True - else: - if socket.error() not in [QLocalSocket.LocalSocketError.ConnectionRefusedError, - QLocalSocket.LocalSocketError.ServerNotFoundError]: - raise SocketError("connecting to running instance", socket) - log.ipc.debug("No existing instance present ({})".format( - debug.qenum_key(QLocalSocket, socket.error()))) - return False - - -def display_error(exc, args): - """Display a message box with an IPC error.""" - error.handle_fatal_exc( - exc, "Error while connecting to running instance!", - no_err_windows=args.no_err_windows) - - def send_or_listen(args): """Send the args to a running instance or start a new IPCServer. diff --git a/qutebrowser/misc/ipcclient.py b/qutebrowser/misc/ipcclient.py new file mode 100644 index 000000000..89d8f3c66 --- /dev/null +++ b/qutebrowser/misc/ipcclient.py @@ -0,0 +1,166 @@ +# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) +# +# SPDX-License-Identifier: GPL-3.0-or-later + +"""Utilities for IPCClient with existing instances.""" + +import os +import json +import getpass +import hashlib + +from qutebrowser.qt.network import QLocalSocket + +import qutebrowser +from qutebrowser.utils import log, error, standarddir, utils, debug + + +CONNECT_TIMEOUT = 100 # timeout for connecting/disconnecting +WRITE_TIMEOUT = 1000 +READ_TIMEOUT = 5000 +ATIME_INTERVAL = 5000 * 60 # 5 minutes +PROTOCOL_VERSION = 1 + + +def _get_socketname_windows(basedir): + """Get a socketname to use for Windows.""" + try: + username = getpass.getuser() + except ImportError: + # getpass.getuser() first tries a couple of environment variables. If + # none of those are set (i.e., USERNAME is missing), it tries to import + # the "pwd" module which is unavailable on Windows. + raise Error("Could not find username. This should only happen if " + "there is a bug in the application launching qutebrowser, " + "preventing the USERNAME environment variable from being " + "passed. If you know more about when this happens, please " + "report this to mail@qutebrowser.org.") + + parts = ['qutebrowser', username] + if basedir is not None: + md5 = hashlib.md5(basedir.encode('utf-8')).hexdigest() + parts.append(md5) + return '-'.join(parts) + + +def _get_socketname(basedir): + """Get a socketname to use.""" + if utils.is_windows: # pragma: no cover + return _get_socketname_windows(basedir) + + parts_to_hash = [getpass.getuser()] + if basedir is not None: + parts_to_hash.append(basedir) + + data_to_hash = '-'.join(parts_to_hash).encode('utf-8') + md5 = hashlib.md5(data_to_hash).hexdigest() + + prefix = 'i-' if utils.is_mac else 'ipc-' + filename = '{}{}'.format(prefix, md5) + return os.path.join(standarddir.runtime(), filename) + + +class Error(Exception): + + """Base class for IPC exceptions.""" + + +class SocketError(Error): + + """Exception raised when there was an error with a QLocalSocket. + + Args: + code: The error code. + message: The error message. + action: The action which was taken when the error happened. + """ + + def __init__(self, action, socket): + """Constructor. + + Args: + action: The action which was taken when the error happened. + socket: The QLocalSocket which has the error set. + """ + super().__init__() + self.action = action + self.code: QLocalSocket.LocalSocketError = socket.error() + self.message: str = socket.errorString() + + def __str__(self): + return "Error while {}: {} ({})".format( + self.action, self.message, debug.qenum_key(QLocalSocket, self.code)) + + +def send_to_running_instance(socketname, command, target_arg, *, socket=None): + """Try to send a commandline to a running instance. + + Blocks for CONNECT_TIMEOUT ms. + + Args: + socketname: The name which should be used for the socket. + command: The command to send to the running instance. + target_arg: --target command line argument + socket: The socket to read data from, or None. + + Return: + True if connecting was successful, False if no connection was made. + """ + if socket is None: + socket = QLocalSocket() + + log.ipc.debug("Connecting to {}".format(socketname)) + socket.connectToServer(socketname) + + connected = socket.waitForConnected(CONNECT_TIMEOUT) + if connected: + log.ipc.info("Opening in existing instance") + json_data = {'args': command, 'target_arg': target_arg, + 'version': qutebrowser.__version__, + 'protocol_version': PROTOCOL_VERSION} + try: + cwd = os.getcwd() + except OSError: + pass + else: + json_data['cwd'] = cwd + line = json.dumps(json_data) + '\n' + data = line.encode('utf-8') + log.ipc.debug("Writing: {!r}".format(data)) + socket.writeData(data) + socket.waitForBytesWritten(WRITE_TIMEOUT) + if socket.error() != QLocalSocket.LocalSocketError.UnknownSocketError: + raise SocketError("writing to running instance", socket) + socket.disconnectFromServer() + if socket.state() != QLocalSocket.LocalSocketState.UnconnectedState: + socket.waitForDisconnected(CONNECT_TIMEOUT) + return True + else: + if socket.error() not in [QLocalSocket.LocalSocketError.ConnectionRefusedError, + QLocalSocket.LocalSocketError.ServerNotFoundError]: + raise SocketError("connecting to running instance", socket) + log.ipc.debug("No existing instance present ({})".format( + debug.qenum_key(QLocalSocket, socket.error()))) + return False + + +def display_error(exc, args): + """Display a message box with an IPC error.""" + error.handle_fatal_exc( + exc, "Error while connecting to running instance!", + no_err_windows=args.no_err_windows) + + +def send(args): + """send a message to IPC Server.""" + socketname = _get_socketname(args.basedir) + try: + sent = send_to_running_instance(socketname, args.command, + args.target) + if sent: + return True + return False + + except Error as e: + display_error(e, args) + raise e diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index 044a6cceb..be71a35ce 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -220,6 +220,16 @@ def _validate_untrusted_args(argv): sys.exit("Found {} after --untrusted-args, aborting.".format(arg)) +def start_new_instance(args): + """start a new instance.""" + from qutebrowser.utils import standarddir + from qutebrowser.misc import ipcclient + + # In order to get socket path for starting a new instance + standarddir.init(args) + return ipcclient.send(args) + + def main(): _validate_untrusted_args(sys.argv) parser = get_argparser() @@ -228,6 +238,8 @@ def main(): if args.json_args is not None: args = _unpack_json_args(args) earlyinit.early_init(args) + if start_new_instance(args): + sys.exit() # We do this imports late as earlyinit needs to be run first (because of # version checking and other early initialization) from qutebrowser import app From 527a5f1c417e91bd6122c4ad0bb7f4c027c026f6 Mon Sep 17 00:00:00 2001 From: Coiby Xu Date: Sun, 24 Jan 2021 20:55:39 +0800 Subject: [PATCH 2/4] fix test_ipc --- qutebrowser/misc/ipc.py | 35 ++--------------------------------- tests/unit/misc/test_ipc.py | 9 +++++---- 2 files changed, 7 insertions(+), 37 deletions(-) diff --git a/qutebrowser/misc/ipc.py b/qutebrowser/misc/ipc.py index 0ab5d76b2..fcf26fc8e 100644 --- a/qutebrowser/misc/ipc.py +++ b/qutebrowser/misc/ipc.py @@ -16,7 +16,8 @@ from qutebrowser.qt.core import pyqtSignal, pyqtSlot, QObject, Qt from qutebrowser.qt.network import QLocalSocket, QLocalServer, QAbstractSocket from qutebrowser.utils import log, usertypes, standarddir, utils, debug, qtutils -from qutebrowser.misc.ipcclient import send_to_running_instance, display_error +from qutebrowser.misc.ipcclient import (Error, SocketError, + send_to_running_instance, display_error) from qutebrowser.qt import sip @@ -69,38 +70,6 @@ def _get_socketname(basedir): return os.path.join(standarddir.runtime(), filename) -class Error(Exception): - - """Base class for IPC exceptions.""" - - -class SocketError(Error): - - """Exception raised when there was an error with a QLocalSocket. - - Args: - code: The error code. - message: The error message. - action: The action which was taken when the error happened. - """ - - def __init__(self, action, socket): - """Constructor. - - Args: - action: The action which was taken when the error happened. - socket: The QLocalSocket which has the error set. - """ - super().__init__() - self.action = action - self.code: QLocalSocket.LocalSocketError = socket.error() - self.message: str = socket.errorString() - - def __str__(self): - return "Error while {}: {} ({})".format( - self.action, self.message, debug.qenum_key(QLocalSocket, self.code)) - - class ListenError(Error): """Exception raised when there was a problem with listening to IPC. diff --git a/tests/unit/misc/test_ipc.py b/tests/unit/misc/test_ipc.py index f611428af..47acf8174 100644 --- a/tests/unit/misc/test_ipc.py +++ b/tests/unit/misc/test_ipc.py @@ -552,7 +552,7 @@ class TestSendToRunningInstance: timeout=5000) as raw_blocker: with testutils.change_cwd(tmp_path): if not has_cwd: - m = mocker.patch('qutebrowser.misc.ipc.os') + m = mocker.patch('qutebrowser.misc.ipcclient.os') m.getcwd.side_effect = OSError sent = ipc.send_to_running_instance( 'qute-test', ['foo'], None) @@ -649,7 +649,7 @@ class TestSendOrListen: @pytest.fixture def qlocalsocket_mock(self, mocker): - m = mocker.patch('qutebrowser.misc.ipc.QLocalSocket', autospec=True) + m = mocker.patch('qutebrowser.misc.ipcclient.QLocalSocket', autospec=True) m().errorString.return_value = "Error string" m.LocalSocketError = QLocalSocket.LocalSocketError m.LocalSocketState = QLocalSocket.LocalSocketState @@ -739,9 +739,10 @@ class TestSendOrListen: with pytest.raises(ipc.Error): ipc.send_or_listen(args) + module_name = "ipcclient" if has_error else "ipc" error_msgs = [ - 'Handling fatal misc.ipc.{} with --no-err-windows!'.format( - exc_name), + 'Handling fatal misc.{}.{} with --no-err-windows!'.format( + module_name, exc_name), '', 'title: Error while connecting to running instance!', 'pre_text: ', From fe50e6d26cf8e73d8b10e0a6cefb9add247bbb62 Mon Sep 17 00:00:00 2001 From: Coiby Xu Date: Sun, 24 Jan 2021 20:59:46 +0800 Subject: [PATCH 3/4] support --temp-basedir for staring a new instance --- qutebrowser/qutebrowser.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index be71a35ce..6ba5cd2fd 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -222,9 +222,13 @@ def _validate_untrusted_args(argv): def start_new_instance(args): """start a new instance.""" + import tempfile from qutebrowser.utils import standarddir from qutebrowser.misc import ipcclient + if args.temp_basedir: + args.basedir = tempfile.mkdtemp(prefix='qutebrowser-basedir-') + # In order to get socket path for starting a new instance standarddir.init(args) return ipcclient.send(args) From 9947f70ba8443f29763c409d091610b273f616e4 Mon Sep 17 00:00:00 2001 From: Coiby Xu Date: Sun, 24 Jan 2021 21:41:17 +0800 Subject: [PATCH 4/4] fix test error --- tests/unit/utils/test_error.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/utils/test_error.py b/tests/unit/utils/test_error.py index 7f3d3a178..404500720 100644 --- a/tests/unit/utils/test_error.py +++ b/tests/unit/utils/test_error.py @@ -24,7 +24,7 @@ class Error(Exception): (ValueError('exception'), 'ValueError', 'exception'), (ValueError, 'ValueError', 'none'), # "qutebrowser." stripped - (ipc.Error, 'misc.ipc.Error', 'none'), + (ipc.Error, 'misc.ipcclient.Error', 'none'), (Error, 'test_error.Error', 'none'), ]) def test_no_err_windows(caplog, exc, name, exc_text):