From 419fc6e1c1500069f13915574ef4cb68415540a3 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 11 Apr 2022 16:53:25 +0200 Subject: [PATCH 1/2] gui init: raise GuiImportError instead of sys.exit if dep is missing --- electrum/__init__.py | 4 ++++ electrum/daemon.py | 6 +++++- electrum/gui/kivy/__init__.py | 9 ++++++--- electrum/gui/qt/__init__.py | 7 +++++-- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/electrum/__init__.py b/electrum/__init__.py index 8bafda636..cea6ac59b 100644 --- a/electrum/__init__.py +++ b/electrum/__init__.py @@ -11,6 +11,10 @@ if is_local and os.name == 'nt': os.add_dll_directory(os.path.dirname(__file__)) +class GuiImportError(ImportError): + pass + + from .version import ELECTRUM_VERSION from .util import format_satoshis from .wallet import Wallet diff --git a/electrum/daemon.py b/electrum/daemon.py index 6c26d6b7e..8bb8fc6b9 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -51,6 +51,7 @@ from .commands import known_commands, Commands from .simple_config import SimpleConfig from .exchange_rate import FxThread from .logging import get_logger, Logger +from . import GuiImportError if TYPE_CHECKING: from electrum import gui @@ -622,7 +623,10 @@ class Daemon(Logger): gui_name = 'qt' self.logger.info(f'launching GUI: {gui_name}') try: - gui = __import__('electrum.gui.' + gui_name, fromlist=['electrum']) + try: + gui = __import__('electrum.gui.' + gui_name, fromlist=['electrum']) + except GuiImportError as e: + sys.exit(str(e)) self.gui_object = gui.ElectrumGui(config=config, daemon=self, plugins=plugins) if not self._stop_entered: self.gui_object.main() diff --git a/electrum/gui/kivy/__init__.py b/electrum/gui/kivy/__init__.py index ab722d4c9..553037673 100644 --- a/electrum/gui/kivy/__init__.py +++ b/electrum/gui/kivy/__init__.py @@ -29,16 +29,19 @@ import sys import os from typing import TYPE_CHECKING +from electrum import GuiImportError + KIVY_GUI_PATH = os.path.abspath(os.path.dirname(__file__)) os.environ['KIVY_DATA_DIR'] = os.path.join(KIVY_GUI_PATH, 'data') try: sys.argv = [''] import kivy -except ImportError: +except ImportError as e: # This error ideally shouldn't be raised with pre-built packages - sys.exit("Error: Could not import kivy. Please install it using the " - "instructions mentioned here `https://kivy.org/#download` .") + raise GuiImportError( + "Error: Could not import kivy. Please install it using the " + "instructions mentioned here `https://kivy.org/#download` .") from e # minimum required version for kivy kivy.require('1.8.0') diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index bf4780b3a..76774fa8d 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -30,12 +30,15 @@ import traceback import threading from typing import Optional, TYPE_CHECKING, List +from electrum import GuiImportError try: import PyQt5 import PyQt5.QtGui -except Exception: - sys.exit("Error: Could not import PyQt5 on Linux systems, you may try 'sudo apt-get install python3-pyqt5'") +except Exception as e: + raise GuiImportError( + "Error: Could not import PyQt5 on Linux systems, " + "you may try 'sudo apt-get install python3-pyqt5'") from e from PyQt5.QtGui import QGuiApplication from PyQt5.QtWidgets import (QApplication, QSystemTrayIcon, QWidget, QMenu, From e47e0afa9165bb0dc4395d40d3aa32ddbb21cf25 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 11 Apr 2022 17:05:26 +0200 Subject: [PATCH 2/2] commands: add "version_info" cmd example: ``` $ ./run_electrum -o version_info { "aiohttp.version": "3.8.1", "aiorpcx.version": "0.22.1", "certifi.version": "2021.10.08", "cryptodome.version": null, "cryptography.path": "/home/user/.local/lib/python3.8/site-packages/cryptography", "cryptography.version": "3.4.6", "dnspython.version": "2.2.0", "electrum.path": "/home/user/wspace/electrum/electrum", "electrum.version": "4.2.1", "hidapi.version": "0.11.0.post2", "libsecp256k1.path": "/home/user/wspace/electrum/electrum/libsecp256k1.so.0", "libusb.path": "libusb-1.0.so", "libusb.version": "1.0.23.11397", "libzbar.path": "/home/user/wspace/electrum/electrum/libzbar.so.0", "pyaes.version": "1.3.0", "pyqt.path": "/usr/lib/python3/dist-packages/PyQt5", "pyqt.version": "5.14.1", "qt.version": "5.12.8" } ``` --- electrum/commands.py | 43 +++++++++++++++++++++++++++++++++-- electrum/crypto.py | 23 ++++++++++++++++++- electrum/ecc_fast.py | 6 +++++ electrum/gui/__init__.py | 6 ++++- electrum/gui/kivy/__init__.py | 9 ++++++++ electrum/gui/qt/__init__.py | 10 ++++++++ electrum/plugin.py | 24 ++++++++++++++++++- electrum/qrscanner.py | 6 +++++ 8 files changed, 122 insertions(+), 5 deletions(-) diff --git a/electrum/commands.py b/electrum/commands.py index 4b1abc77a..edd971616 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -38,6 +38,7 @@ from functools import wraps, partial from itertools import repeat from decimal import Decimal from typing import Optional, TYPE_CHECKING, Dict, List +import os from .import util, ecc from .util import (bfh, bh2u, format_satoshis, json_decode, json_normalize, @@ -57,11 +58,13 @@ from .lnutil import SENT, RECEIVED from .lnutil import LnFeatures from .lnutil import extract_nodeid from .lnpeer import channel_id_from_funding_tx -from .plugin import run_hook +from .plugin import run_hook, DeviceMgr from .version import ELECTRUM_VERSION from .simple_config import SimpleConfig from .invoices import LNInvoice from . import submarine_swaps +from . import GuiImportError +from . import crypto if TYPE_CHECKING: @@ -546,9 +549,45 @@ class Commands: @command('') async def version(self): """Return the version of Electrum.""" - from .version import ELECTRUM_VERSION return ELECTRUM_VERSION + @command('') + async def version_info(self): + """Return information about dependencies, such as their version and path.""" + ret = { + "electrum.version": ELECTRUM_VERSION, + "electrum.path": os.path.dirname(os.path.realpath(__file__)), + } + # add currently running GUI + if self.daemon and self.daemon.gui_object: + ret.update(self.daemon.gui_object.version_info()) + # always add Qt GUI, so we get info even when running this from CLI + try: + from .gui.qt import ElectrumGui as QtElectrumGui + ret.update(QtElectrumGui.version_info()) + except GuiImportError: + pass + # Add shared libs (.so/.dll), and non-pure-python dependencies. + # Such deps can be installed in various ways - often via the Linux distro's pkg manager, + # instead of using pip, hence it is useful to list them for debugging. + from . import ecc_fast + ret.update(ecc_fast.version_info()) + from . import qrscanner + ret.update(qrscanner.version_info()) + ret.update(DeviceMgr.version_info()) + ret.update(crypto.version_info()) + # add some special cases + import aiohttp + ret["aiohttp.version"] = aiohttp.__version__ + import aiorpcx + ret["aiorpcx.version"] = aiorpcx._version_str + import certifi + ret["certifi.version"] = certifi.__version__ + import dns + ret["dnspython.version"] = dns.__version__ + + return ret + @command('w') async def getmpk(self, wallet: Abstract_Wallet = None): """Get master public key. Return your wallet\'s master public key""" diff --git a/electrum/crypto.py b/electrum/crypto.py index 8fc02a8d8..fd255c8f0 100644 --- a/electrum/crypto.py +++ b/electrum/crypto.py @@ -28,7 +28,7 @@ import os import sys import hashlib import hmac -from typing import Union +from typing import Union, Mapping, Optional from .util import assert_bytes, InvalidPassword, to_bytes, to_string, WalletFileException, versiontuple from .i18n import _ @@ -84,6 +84,27 @@ if not (HAS_CRYPTODOME or HAS_CRYPTOGRAPHY): sys.exit(f"Error: at least one of ('pycryptodomex', 'cryptography') needs to be installed.") +def version_info() -> Mapping[str, Optional[str]]: + ret = {} + if HAS_PYAES: + ret["pyaes.version"] = ".".join(map(str, pyaes.VERSION[:3])) + else: + ret["pyaes.version"] = None + if HAS_CRYPTODOME: + ret["cryptodome.version"] = Cryptodome.__version__ + if hasattr(Cryptodome, "__path__"): + ret["cryptodome.path"] = ", ".join(Cryptodome.__path__ or []) + else: + ret["cryptodome.version"] = None + if HAS_CRYPTOGRAPHY: + ret["cryptography.version"] = cryptography.__version__ + if hasattr(cryptography, "__path__"): + ret["cryptography.path"] = ", ".join(cryptography.__path__ or []) + else: + ret["cryptography.version"] = None + return ret + + class InvalidPadding(Exception): pass diff --git a/electrum/ecc_fast.py b/electrum/ecc_fast.py index 3a6e706e4..d95234aa0 100644 --- a/electrum/ecc_fast.py +++ b/electrum/ecc_fast.py @@ -138,3 +138,9 @@ except BaseException as e: if _libsecp256k1 is None: # hard fail: sys.exit(f"Error: Failed to load libsecp256k1.") + + +def version_info() -> dict: + return { + "libsecp256k1.path": _libsecp256k1._name if _libsecp256k1 else None, + } diff --git a/electrum/gui/__init__.py b/electrum/gui/__init__.py index 4d4ccdcdd..4d366879c 100644 --- a/electrum/gui/__init__.py +++ b/electrum/gui/__init__.py @@ -4,7 +4,7 @@ # Notifications about network events are sent to the GUI by using network.register_callback() -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Mapping, Optional if TYPE_CHECKING: from . import qt @@ -28,3 +28,7 @@ class BaseElectrumGui: This method must be thread-safe. """ pass + + @classmethod + def version_info(cls) -> Mapping[str, Optional[str]]: + return {} diff --git a/electrum/gui/kivy/__init__.py b/electrum/gui/kivy/__init__.py index 553037673..b7c9249a6 100644 --- a/electrum/gui/kivy/__init__.py +++ b/electrum/gui/kivy/__init__.py @@ -80,3 +80,12 @@ class ElectrumGui(BaseElectrumGui, Logger): if not app: return Clock.schedule_once(lambda dt: app.stop()) + + @classmethod + def version_info(cls): + ret = { + "kivy.version": kivy.__version__, + } + if hasattr(kivy, "__path__"): + ret["kivy.path"] = ", ".join(kivy.__path__ or []) + return ret diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index 76774fa8d..9ff817b8f 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -472,3 +472,13 @@ class ElectrumGui(BaseElectrumGui, Logger): def stop(self): self.logger.info('closing GUI') self.app.quit_signal.emit() + + @classmethod + def version_info(cls): + ret = { + "qt.version": QtCore.QT_VERSION_STR, + "pyqt.version": QtCore.PYQT_VERSION_STR, + } + if hasattr(PyQt5, "__path__"): + ret["pyqt.path"] = ", ".join(PyQt5.__path__ or []) + return ret diff --git a/electrum/plugin.py b/electrum/plugin.py index 2d5592d16..fe7b93932 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -29,7 +29,7 @@ import time import threading import sys from typing import (NamedTuple, Any, Union, TYPE_CHECKING, Optional, Tuple, - Dict, Iterable, List, Sequence, Callable, TypeVar) + Dict, Iterable, List, Sequence, Callable, TypeVar, Mapping) import concurrent from concurrent import futures from functools import wraps, partial @@ -749,3 +749,25 @@ class DeviceMgr(ThreadJob): client.handler.update_status(False) return devices + + @classmethod + def version_info(cls) -> Mapping[str, Optional[str]]: + ret = {} + # add libusb + try: + import usb1 + except Exception as e: + ret["libusb.version"] = None + else: + ret["libusb.version"] = ".".join(map(str, usb1.getVersion()[:4])) + try: + ret["libusb.path"] = usb1.libusb1.libusb._name + except AttributeError: + ret["libusb.path"] = None + # add hidapi + from importlib.metadata import version + try: + ret["hidapi.version"] = version("hidapi") + except ImportError: + ret["hidapi.version"] = None + return ret diff --git a/electrum/qrscanner.py b/electrum/qrscanner.py index dab4e87de..424dd4186 100644 --- a/electrum/qrscanner.py +++ b/electrum/qrscanner.py @@ -102,5 +102,11 @@ def find_system_cameras() -> Mapping[str, str]: return devices +def version_info() -> Mapping[str, Optional[str]]: + return { + "libzbar.path": libzbar._name if libzbar else None, + } + + if __name__ == "__main__": print(scan_barcode())